Homework 4 Solution

Due Date: Aug 8 before midnight; No Late Submissions
Max Points: 100

Important Reminder: As per the course Academic Honesty Statement, cheating of any kind will minimally result in your letter grade for the entire course being reduced by one level.

To be turned in via the submission link on brightspace.

Please remember to justify all answers.

You are encouraged to use the web or the library but are required to cite any external sources used in your answers.

It may be the case that some questions cannot be answered as requested.

  1. If you followed the hints for Project 4 or Project 5, validating user entry into a spreadsheet cell entails a web service call. This is not particularly efficient. Discuss changes to the implementation to remove this inefficiency. Your approach should allow reuse of the validation code developed in earlier projects. 5-points

    The simplest solution is to build up a library which is a subset of the cs544-prj2-sol library with all server dependencies removed. Specifically, include spreadsheet.ts (which provides all the validation logic) and its direct and indirect dependancies expr-parser.ts, utils.ts and limits.ts. Note that none of these files have any server-side dependencies.

    Considering the fact that the web services ss-ws.ts provide the same interface as ss-services.ts, it would be useful to also include ss-services.ts, but that has a clear server-side dependancy on spreadsheet-dao.ts. However, since an ss-services.ts has an instance of SpreadsheetDao injected into it, it would be simple enough to replace the injection with an in-memory version of SpreadsheetDao implemented simply as { [ ssName: string ]: { [cellId: string] : string }}.

    The services in ss-ws.ts can first call the corresponding service in this modified ss-services.ts. The web request would be made only if that call returns success. If the call indicates a validation error, then the web service call is never made. This permits validation in the client using the validation code developed in earlier projects.

  2. Discuss the choice of HTML form widgets for each of the following situations: 15-points

    1. Selecting one-or-more US states.

    2. Specifying a color.

    3. Specifying the speed of an animation.

    4. Specifying a person's age in years.

    5. Selecting one-or-more planets from our solar system.

    The answers follow:

    1. Selecting multiple elements from a large list is problematic on desktop devices and even worse on mobile devices. One aspect of this particular problem is that the user will be familiar with the possible US states and is not likely to get bogged down selecting from an unfamiliar list.

      The ideal control would be a select element with the multiple attribute set <select multiple="multiple">. Unfortunately, most users are not familiar with choosing multiple options from a select box; hence if this option is specified, it is a good idea to provide brief help text or a tool tip.

      If the maximum number of states to be selected is known and small, then multiple uni-select boxes could be used, with labels like Select First State and Select Second State.

      Any kind of select box with 50+ selections will likely be clumsy on mobile devices.

      Another possibility would be to use checkboxes <input type="checkbox">. However, this would have the following disadvantages:

      • Would add clutter to the UI.

      • Would use up too much screen real-estate.

      If screen real-estate is not a problem, then it is possible to hide the checkboxes behind an image map with the image of each state corresponding to a checkbox. This would remove some of the clutter from the UI.

      If the maximum number of states to be selected is known and small, then another possibility is to use an input with a datalist.

      	<datalist id="states">
      	  <option>AL</option>
      	  <option>AK</option>
      	  ...
      	</datalist
      
      	<label for="state1">Enter first state</label>
      	<input name="state1" list="states">
      
      	<label for="state2">Enter second state</label>
      	<input name="state2" list="states">
      

      In summary, though the above alternatives can be used, there does not appear to be any good solution which does not use JavaScript.

    2. In modern browsers, we would use <input type="color">.

    3. Since the animation speed need not be precise, in modern browsers we can use a slider <input type="range">.

    4. In modern browsers, we can use something like

      	<input type="number" min="0" max="120" step="1">
      
    5. Since the number of planets is relatively small, <input type="checkbox"> would probably suffice.

  3. Discuss the tradeoffs between choosing the DOMContentLoaded browser event versus the load browser event. Give examples of situations where you would prefer one over the other. 10-points

    The DomContentLoaded event is fired after the DOM tree is built but external resources like stylesheets and images may not have loaded, whereas the load event fires only after the DOM tree is built and external resources have loaded. It follows that:

    • The DomContentLoaded event handler may run before the load event handler, leading to a more zippy UX.

    • DomContentLoaded should be preferred over load except for situations where the event handler requires access to the external resources.

    Hence if we just need to attach event handlers to DOM elements, we would use an event handler triggered by DomContentLoaded event. OTOH, if we needed to resize portions of a page based on the size of an external image, we would use an event handler triggered by the load event.

  4. A shopping cart is displayed using HTML structured as follows:

         <div class="cart" id="cart123">
           <ul class="cart-items">
      	 <li>
    	   <span class="item-descr" data-product-id="2347">
    	     Beefsteak tomatoes
    	   </span>
    	   <span class="item-quantity" data-unit="pound">
    	     2.34 pounds
    	   </span>
    	   <span class="item-price">$ 5.63</span>
    	 </li>
    	 <!-- more <li> items with similar structure -->
    	 ...
           </ul>
         </div>
    

    Using only the DOM API, provide JavaScript code for each of the following:

    1. Show code for a function cartItems(cardId) which returns a list containing all the items in the cart having id cartId. For example, given the above data, the returned list for cartItems('cart123') should start out as [ 'Beefsteak tomatoes', ... ].

    2. Show code for a function itemPrice(cartId, itemIndex) which returns a number giving the price of the item at index itemIndex in the cart with id cartId. For example, given the above data, itemPrice('cart123', 0) should return 5.63.

    3. Show code for a function itemUnit(cartId, itemIndex) which returns the data-unit attribute of the item at index itemIndex. For example, itemUnit('cart123', 0) should return 'pound'.

    4. Show code for a function cartPrice(cartId) which returns the total price of the cart (each item contributes unit-price * item-quantity to the total price of the cart).

    5. Using the function from the previous part, show code which appends the total price of the cart between the closing </ul> and the closing </div> using any reasonable HTML formatting.

    If any of the above order-items do not exist, then the above functions should return null. 15-points

    Working code (not required). The relevant parts are reproduced below:

    1. The code for the function follows:

             /** return list of all order items */
             function cartItems(cartId) { 
               const sel = `#${cartId} .item-descr`;
               const nodes = [...document.querySelectorAll(sel)];
               return nodes.map(e => e.innerHTML.trim());
             }
      
    2. The code for the function follows:

             /** return number giving price of item */
             function itemPrice(cartId, itemIndex) { 
        	 const sel = `#${cartId} .item-price`;
      	 const nodes = document.querySelectorAll(sel);
      	 const price = nodes[itemIndex]?.innerHTML;
      	 return price ? Number(price.replace(/[^\d\.]/g, '')) : null;
             }
      
    3. The code for the function follows:

             /** return # of units for item in cart */
             function itemUnit(cartId, itemIndex) { 
               const sel = `#${cartId} .item-quantity`;
               const el = document.querySelectorAll(sel)[itemIndex];
               return (el) ? el.getAttribute('data-unit') : null;
             }
      
    4. The code for the function follows:

             /** return total price of cart */
             function cartPrice(cartId) {
               const innerHTMLNumber = 
                 el => Number(el.innerHTML.replace(/[^\d\.]/g, ''));
               const sel1 = `#${cartId} .item-price`;
               const unitPrices = 
                 [...document.querySelectorAll(sel1)].map(innerHTMLNumber);
               const sel2 = `#${cartId} .item-quantity`;
               const quantities = 
                 [...document.querySelectorAll(sel2)].map(innerHTMLNumber);
               console.assert(unitPrices.length === quantities.length,
                              `failed ${unitPrices.length} === ${quantities.length}`);
               const total = unitPrices
                 .reduce((acc, p, i) => acc + (p * quantities[i]), 0);
               return Number(total.toFixed(2));
             }
      
    5. The code for a function which does the append follows:

             /** append total cost */
             function appendTotalPrice(cartId) {
               const total = this.cartPrice(cartId);
               const cart = document.querySelector(`#${cartId}`);
               const text =
                 document.createTextNode(`Total price: $ ${total}`);
               cart.appendChild(text);
               return '';
             }
      
  5. The management of a large multi-national corporation is aware of the need to keep its programmers well caffeinatated. Being a woke, environmentally conscious corporation, the management prefers to not use single-brew solutions like K-cups but uses old fashioned coffee pots instead. In the absence of widespread adoption of RFC 2324, the company decides to use HTTP web services with the following services:

    GET /coffee

    Returns a JSON list of all the company locations having coffee pots which are hooked into the web services.

    Example output: [ "Boston", "Johannesburg", "London", "Mumbai", "San Diego", "Sao Paulo" ]

    GET /coffee/LOC

    Returns a JSON list of office areas at LOC having coffee pots.

    For example, GET /coffee/San%20Diego returns [ "Kitchen 1", "Kitchen 2", "Lobby" ].

    GET /coffee/LOC/AREA

    Returns the status of the coffee pot in area AREA at location LOC. Specifically, it returns a JSON pair [ FULL_PERCENT, BREW_MINUTES ] where FULL_PERCENT is a percentage between 0 and 100 specifying how full the coffee pot is and BREW_MINUTES specifies how many minutes ago the coffee was freshly brewed.

    For example, GET /coffee/San%20Diego/Kitchen%202 may return [ 67, 18 ].

    Ignoring errors, these web services can be accessed in a browser using the following wrapper API:

        async function getLocations() : string[];
    
        async function getLocationAreas(loc: string) : string[];
    
        async function getAreaCoffeePotStatus(loc: string, area: string)
          : [ number, number ];
    

    with a simulated implementation.

    Discuss how you would use the DOM API to implement a single page app (SPA) which allows any employee of the corporation to check the status of coffee pots. Specifically, the UI should allow the employee to specify a location. Then at one second intervals, the page should continuously cycle through all the areas at that location, displaying the coffee pot status for that area. A typical UI is shown in this brief video.

    Your discussion should minimally include the following:

    • The events which would be caught along with a brief description of the operation of the corresponding event handler.

    • The initial setup of the SPA.

    [If you decide to actually write code and submit a .html file, then please be aware of the fact that brightspace silently replaces all <script> sections of HTML with its own content. A workaround for this totally unacceptable behavior is to add a .text extension to the file so that it is submitted as .html.text] 20-points

    On startup in reponse to the DOMContentLoaded event, the SPA will:

    1. Instantiate the coffee web services.

    2. Call the getLocations() web service to get the locations for the company with coffee pots participating in the web service.

    3. Render a select widget listing those locations as its options. Choose the first location as the current location.

    4. Get the coffee pot areas for the currently selected location using the getLocationAreas() web service.

    5. Create an interval timer to run every second.

    A change listener for the select widget will use the getLocationAreas() web service to update the coffee pot areas for the currently selected location.

    Each tick of the interval timer will use the getAreaCoffeePotStatus() web service for the current area and cycle the areas. The coffee pot status will be displayed on the page.

    A working version (not required) is available in dom-coffee.html.

  6. Repeat the previous question, but use React hooks instead of the DOM API. Specify the hooks you would use; for useEffect() be sure to specify the dependancy array and cleanup function (if needed). 20-points

    We can use a single App component having the following state constituents set up using the useState() hook:

    locs

    the set of locations.

    loc

    the currently selected location.

    areas

    the set of areas for loc.

    index

    the index of the currently displayed area.

    status

    the coffee pot status pair for the currently displayed area.

    For initialization we call the useEffect() hook with an empty dependancy array. It makes a getLocations() web service call and sets the locs and loc state constituents. Subsequently, it makes an additional getLocationAreas() to get and set the areas state constituent for loc.

    The initialization effect also sets up an interval timer which bumps up the index state constituent. The hook returns a function which cleans up the interval timer.

    When the select box rendering the locs is changed, the loc and areas state constituents are updated (the latter requires a call to the getLocationAreas() web service).

    We use an effect which depends on index and areas to respond to changes in the areas (caused by changing the select box) and the index (caused by the tick of the interval timer) state constituents. This effect updates the coffee pot status using the getAreaCoffeePotStatus() web service.

    A working version (not required) is available in react-coffee.html.

  7. Explain precisely why the JavaScript expression '2' * 3 + 4 + '5' + 6 results in '1056'. 5-points

    Making the associativity and precedence of the operators explicit by inserting parentheses, the expression is equivalent to ((('2' * 3) + 4) + '5') + 6. In '2' * 3, the '2' is converted to a number resulting in number 6. Hence in 6 + 4 the + is interpreted as addition, result in number 10. Next, 10 + '5', the string '5' causes the + to be interpreted as concatentation, resulting in string '105'. Finally, in '105' + 6, the string '105' causes the + to be interpreted as concatentation resulting in final result string '1056'.

  8. Two programmers Bill and Mary are working at a company bigcorp. Mary has a cool web service running on her workstation such that a GET to the URL http://mary.bigcorp.com/company-photos returns a random png image corresponding to a photo taken at a company event. Bill would like to use this web service to display a random company event photo whenever his home page at http://bill.bigcorp.com is accessed. He inserts a call to access Mary's web service in the JavaScript contained on his homepage but it does not work. Give possible reasons for this problem. 10-points

    Some possible reasons:

    • Since http://mary.bigcorp.com is a different origin from http://bill.bigcorp.com, the same-origin policy will not allow JavaScript from Bill's domain to make a call to Mary's domain. A fix for that would be to have Mary set up CORS exceptions to whitelist Bill's domain.

    • Since the domain is bigcorp.com, it is possible that the company is a large corporation. Large corporations run rather restrictive firewall policies. Since most employees use their workstations purely as web clients, the corporation's security policies probably do not allow use of a workstation as a server by another workstation. So it may be impossible for Bill's homepage to access Mary's web services without some modification of the company firewall rules.