WDD 330: Web Frontend Development II

W02 Team Activity: Dynamic Product Detail Page

Overview

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

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 team has given that section a try. Then after you look at the example, resist the temptation to copy/paste. Use the examples to make corrections or to help the team move forward. 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

There is currently an HTML file for each of the four tent products in the product_pages directory. This organization is fine when there are only a few products. The plan is to expand the inventory and therefore, this webpage per product model does not work.

The product pages all follow a similar layout and appearance so there should be only one HTML file that dynamically loads the details about the requested product.

In addition, as a web application grows, using ES Modules becomes increasingly important. ES Modules offer better organization and code reusability, which are crucial aspects for managing complex software projects.

Remember that the words directory and folder have essentially the same meaning. Directory is the more accurate term for file systems while folder 📂 refers to the graphical metaphor that is generally accepted because it is highly related to the term file.

IMPORTANT!

Make sure to work in the src directory when completing this activity and NOT the dist directory! The build directory will get erased and completely rebuilt each time the npm run build is run. 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. In the product_pages directory, copy one of the tent product pages by opening it in the editor and then use
    File> Save As... and name the file index.html saving it in the product_pages directory.
  2. Add a new file, a module named ProductDetails.mjs in the js directory.
    This script file will be programmed to contain the code to dynamically produce the product detail pages.
  3. It's very likely that other parts of the 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.

    The 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 the module portable and easy to use, it uses a class to expose all the public facing code in the module. The class is called ProductData, and it is the default export.
  2. The ProductsDetails class will be used for more than just tents. When a category name, for example, 'tents', is passed to the class, the class will store it and use it to build a path to the correct JSON file.
    this.path = `../json/${this.category}.json`;
  3. Notice the getData() method compared to the findProductById() method. The getData() method uses just promises and .then(), the findProductById() method uses async/await.

    In general, the async/await syntax is considered to be easier to read (and write) than the typical .then() based promise handling.

  4. If the syntax (data) => data is confusing, spend some time reading about arrow functions. This short callback function can be rewritten to be like 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 ES Modules in JavaScript a type="module" attribute must be included in the HTML script tag.

URL Parameters

There needs to be some way of passing in the product to show the details. A common way to do this is through a URL Parameter or query string. The following example HTML anchor element (hyperlink) has the product ID embedded into the link and named "product". When the page loads it will have access to that information.

<a href="product_pages/index.html?product=880RR" >

The following statements:

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 getParam(param) that you can use to return a parameter from the URL when requested.
  2. Import this new getParam 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? Look in the json/tents.json file and note that each record has an id value. 880RR just happens to be the id for the first tent.

  4. Test the getParam function in product.js to see test if the productId displays in the URL when a product is clicked. This would be a good time to test out the findProductById method as well.
    import { setLocalStorage, getParam } from './utils.mjs';
    import ProductData from './ProductData.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. Open your new file ProductDetails.mjs which located in the js directory.
  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 the class can be used. Some will happen in the constructor, automatically. Other Others need to be controlled and will be placed in this init method.
    3. addProductToCart(): This is the function that is currently in product.js.
      Move it here and note that project.js does not need to import setLocalStorage from utils.mjs anymore. This
    4. renderProductDetails(): Method to generate or populate the HTML to display the product details.
  3. It will be nice for the 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 retrieved details.
  4. Refactor the product.js code to pull everything from the last few steps together.
    • Import the functions that are needed in this module from utils.mjs, ProductData.mjs, and ProductDetails.mjs? You may need to come back to add these as you review the other requirements.
    • Get the product parameter, the product id, from the URL using the helper function getParam
    • Create an instance of the ProductData data class with the URL it should use to look for products.
    • Use both of these variables to create an instance of the ProductDetails class
    • Call the init() method using the class instance to finish setting everything up.
    Once everything is refactored the file should look something like this:
    import { getParam } from './utils.mjs';
    import ProductData from './ProductData.mjs';
    import ProductDetails from './ProductDetails.mjs';
    
    
    const productId = getParam('product');
    const dataSource = new ProductData('tents');
    
    const product = new ProductDetails(productId, dataSource);
    product.init();

Finish It Up!

  1. Develop the rest of the methods in the ProductDetails class to make product selection work.
    • init(): This class initializer method needs to fetch product details using the provided data source and render the details. In addition, this method needs to handle the user clicking the 'Add to Cart' button.
      Use the following comments as a guide in the development of this method.
      async init() {
      // use the datasource to get the details for the current product. findProductById will return a promise! use await or .then() to process it
      // the product details are needed before rendering the HTML
      // once the HTML is rendered, add a listener to the Add to Cart button
      // Notice the .bind(this). This callback will not work if the bind(this) is missing. Review the readings from this week on 'this' to understand why.
      document.getElementById('addToCart')
        .addEventListener('click', this.addToCart.bind(this));
      }
    • addProductToCart(): Move this function from product.js. Make any changes necessary to make it work. This method will be called from within the init() method.
    • renderProductDetails(): Use the HTML in the /product_pages/index.html as a guide to the HTML that needs to be generated. This method will also be called from within the init() method.

Example Solution

In this team activity, you are expected to look over a solution from the instructor, to compare your approach to one provided.

Do NOT open the solution example 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 example solution and use it to help you complete your own code.

Example Solution

The example solution can be found in the src directory: WDD 330 Sleep Outside Repository week2-Team branch.

Make a Pull Request

  1. Complete as much as you can on this activity.
  2. Review the example 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. Someone move the Trello task card to the "Done" list.

Submission

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