Skip to content

Instantly share code, notes, and snippets.

@travisluong
Last active August 29, 2015 14:21
Show Gist options
  • Save travisluong/e846bf9a94ff074d7e23 to your computer and use it in GitHub Desktop.
Save travisluong/e846bf9a94ff074d7e23 to your computer and use it in GitHub Desktop.

Donut Shop Tutorial

Spoiler Alert!!! If you wish to challenge yourself, please try to complete the assignment without referring to this article. Going through the struggle of trying to get stuff to work and fixing bugs is one of the best ways to learn code. But if you are truly stuck, then go ahead and read this tutorial.

This tutorial will show you how to build a basic "donut shop" website to complete the homework assignment. We will use HTML5, CSS, JavaScript, and jQuery. As with any real world web development projects, requirements can often be vague and open to interpretation. This is just one interpretation of the homework requirements. If what you have so far is different from this, it doesn't mean it's wrong. I would suggest taking the concepts you gain from this tutorial and applying them to what you already have. Please don't copy and paste. If you do go through this tutorial, try to make it fit your original vision of the assignment.

View the completed webpage.

Step 1: Mark Up The Page

First, fill out your page with the standard HTML skeleton. Then, within your body tags, add the following HTML markup. We will use semantic HTML5 markup here. Later, we will dynamically generate the nav and content. You may notice a "clearfix" class on some of the elements. I'd recommend googling it if you are unfamiliar with it, but it basically fixes layout breakage when you have "floated" elements within an element.

<div id="page" class="clearfix">

  <header id="header">
    <h1>Donut Fellows.</h1>
    <h2>Learn to make donuts. Yumminess. Guaranteed.</h2>
  </header>

  <nav id="nav" class="clearfix">
    
  </nav>

  <section id="content" class="clearfix">
    <h3>Hello. Choose a donut shop to get started.</h3>
  </section>

  <footer id="footer">
    &copy; 2015 Donut Fellows
  </footer>

</div>

Step 2: Add jQuery

Add this line just above your closing body tag. It will load the the jQuery file from a CDN. That just means it is being pulled from a Content Delivery Network. It'll find the server closest to you and send the file to you from that server. You must have internet for it to load correctly. And just as a reminder, jQuery is a JavaScript library, which is a bunch of pre-written JavaScript code with a lot of common functionality packaged into one file. We'll be using jQuery a lot in this tutorial.

<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>

Step 3: Add some CSS styling to the page

For now, just copy and paste this into an external css file and link it to your page or drop everything into a style tag. You can choose whatever you prefer. It's generally better practice to keep all CSS in another file. Just a few things to note: We went with a 960px page width and centered it with margin-auto. We float the 200px nav element and the 700px content element to the left. The "display" property has been changed to "block" on several elements. There are other display types like inline and inline-block. Each one makes the element behave differently, so I'd recommend googling about it if you're unfamiliar with it.

body {
  background: #FFFFFF;
}
form {
  background: #468966;
  padding: 20px;
}
#page {
  background: #FFF0A5;
  width: 960px;
  margin: auto;
  padding: 20px;
}
#header {
  background: #B64926;
  padding: 20px;
  margin-bottom: 20px;
}
#nav {
  width: 200px;
  float: left;
}
#nav ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
}
#nav ul li {
  margin: 0;
  margin-bottom: 1px
}
#nav button {
  width: 200px;
  display: block;
  border-radius: 0;
  border: none;
  background: #468966;
  cursor: pointer;
  color: #FFFFFF;
}
#nav button:hover {
  background: #B64926;
}
#content {
  width: 700px;
  float: left;
  background: #FFB03B;
  margin-left: 20px;
  padding: 20px;
}
#footer {
  background: #8E2800;
  width: 920px;
  padding: 20px;
  float: left;
  margin: 20px 0;
  color: #FFFFFF;
}
.clearfix {
  overflow: auto;
}

Step 4: The DonutShop constructor

Just a quick review about constructors: It has always helped me to think of constructors as a factory for the particular object you are constructing. In our case we have a DonutShop factory that produces donut shops. We use the new keyword to instantiate a new donut shop like so var shop = new DonutShop("Downtown", 5, 10, 12, 8). "Instantiate" is just a technical term meaning to create an object using a constructor. We are using the DonutShop constructor to "instantiate" a DonutShop object. We can use it to create as many donut shop objects as we want and they'll all have the same methods (behaviors). They can also have varying properties by the parameters that we pass into the constructor when we instantiate the donut shop.

Just a quick review about this. What is this? this refers to the current object. this can get confusing especially when you start working with callbacks which we will later in this tutorial. If you're ever unsure of what this is referring to, just console.log(this) in your code to check what it is referring to. In the code below, this is referring to the DonutShop instance. So when we call shop.donutsPerHour() the code in that function will know to check that specific "Downtown" DonutShop instance.

function DonutShop(shopName, minCustomersPerHour, maxCustomersPerHour, avgDonutsPerCustomers, hoursOpenPerDay) {
    this.shopName = shopName;
    this.minCustomersPerHour = minCustomersPerHour;
    this.maxCustomersPerHour = maxCustomersPerHour;
    this.avgDonutsPerCustomers = avgDonutsPerCustomers;
    this.hoursOpenPerDay = hoursOpenPerDay;
    this.customersPerHour = function() {
      return Math.floor(Math.random() * (this.maxCustomersPerHour - this.minCustomersPerHour + 1) + this.minCustomersPerHour);
    }
    this.donutsPerHour = function() {
      return this.customersPerHour() * this.avgDonutsPerCustomers;
    }
    this.donutsPerDay = function() {
      return this.donutsPerHour() * this.hoursOpenPerDay;
    }      
}

Step 5: The DonutMaster constructor

It seems there has been much confusion as to what the DonutMaster really means in the context of this assignment. It does not mean a donut shop manager since it doesn't have properties representing a real person. It is actually more like a "controller". If you are learning web development, you will inevitably come across the term MVC, which stands for Model View Controller. The controller is the glue that communicates with views and models. The model in our case is the donut shop. The view is the rendered HTML that the user interacts with.

For a simple webpage like this, we can get away with using plain old JavaScript and jQuery to build out our forms and logic. But if you start working with single page applications, you might want to use a front-end MV* framework like Backbone.js or AngularJS, as it will provide greater organization for your code. Nevertheless, it is extremely useful to know how to build stuff from scratch because it will help you understand why you would even need an MV* framework in the first place.

// This is the Donut Master class. It is responsible for managing the collection of DonutShop objects, keeping track of user actions, and updating the HTML on the page.
function DonutMaster() {

  // Initialize the donutShops array. 
  // Select the nav, ul, and content elements from the page. This is a good practice so that we don't have to waste resources selecting the elements again later.
  // Store a reference to the current DonutMaster object to donutMaster variable. We need to do this so that we can access the DonutMaster instance from within an event handler later on.
  this.donutShops = [];
  var nav = $('#nav');
  var navUl = $('<ul>');
  var content = $('#content');
  var donutMaster = this;

  // This method allows us to add a DonutShop object to the donutShops array.
  this.addShop = function(shop) {
    this.donutShops.push(shop);
  }


  // We create this method since finding a shop is something that we may do more than once in this program. 
  // Remember DRY, Don't Repeat Yourself. 
  // If you ever have to write a piece of code twice, extract it into a function.
  this.findShopByShopName = function(shopName) {
    var shop = donutMaster.donutShops.filter(function(shop) {
      return shop.shopName == shopName;
    })[0];

    return shop;
  }

  // This method will generate the shop list and put it into the nav.
  this.generateShopList = function() {
    for (var i = 0; i < this.donutShops.length; i++) {
      var button = $('<button>').text(this.donutShops[i].shopName);
      var li = $('<li>').append(button);
      navUl.append(li)
    };
    nav.append(navUl);
  }

  // This method loops through all of the buttons and attaches a click even listener to it. 
  // The event handler will find the shop from the donutShops array based on the button text, and then update the details and form on the page.
  // Note that we are calling the methods on the donutMaster variable that we declared earlier.
  // If we simply tried to call the methods on "this", it would have thrown an error because it would have referred to the button element!
  this.bindButtons = function() {
    navUl.find("button").each(function(index) {
      $(this).on('click', function(e) {
        var shopName = $(this).text();
        var shop = donutMaster.findShopByShopName(shopName);
        donutMaster.showShopDetails(shop);
        donutMaster.showShopForm(shop);
      });
    });
  }

  // This method takes a shop as parameter and then generates the details HTML and appends it to the page. 
  // It will empty the content div before appending the new details.
  this.showShopDetails = function(shop) {
    var h2 = $('<h2>').append(shop.shopName);
    var minCustomersPerHour = $('<p>Min Customers Per Hour: </p>').append(shop.minCustomersPerHour);
    var maxCustomersPerHour = $('<p>Max Customers Per Hour: </p>').append(shop.maxCustomersPerHour);
    var avgDonutsPerCustomers = $('<p>Average Donuts Per Customer: </p>').append(shop.avgDonutsPerCustomers);
    var hoursOpenPerDay = $('<p>Hours Open Per Day: </p>').append(shop.hoursOpenPerDay);
    var donutsPerHour = $('<p>Donuts Per Hour: </p>').append(shop.donutsPerHour()); // 20
    var donutsPerDay = $('<p>Donuts Per Day: </p>').append(shop.donutsPerDay());
    content.empty();
    content.append(h2);
    content.append(minCustomersPerHour);
    content.append(maxCustomersPerHour);
    content.append(avgDonutsPerCustomers);
    content.append(hoursOpenPerDay);
    content.append(donutsPerHour);
    content.append(donutsPerDay);
  }

  // This method generates a form and then calls bindForm to attach the submit event listener to it. 
  // Then it appends it to the page.
  this.showShopForm = function(shop) {
    var form = $('<form>')
    var formMinCustomerInput = $('<label name="minCustomers">Min Customers Per Hour</label><br><input type="text" name="minCustomers"><br><br>');
    var formMaxCustomerInput = $('<label name="maxCustomers">Max Customers Per Hour</label><br><input type="text" name="maxCustomers"><br><br>');
    var formAvgDonutsPerCustomer = $('<label name="avgDonuts">Avg Donuts Per Customer</label><br><input type="text" name="avgDonuts"><br><br>');
    var formHoursOpenPerDay = $('<label name="hoursOpen">Hours Open Per Day</label><br><input type="text" name="hoursOpen"><br><br>');
    var submit = $('<input type="submit" value="Recalculate">')

    this.bindForm(form, shop);

    form.append(formMinCustomerInput);
    form.append(formMaxCustomerInput);
    form.append(formAvgDonutsPerCustomer);
    form.append(formHoursOpenPerDay);
    form.append(submit);
    content.append("<h2>Edit Shop</h2>")
    content.append(form);
  }

  // This method attaches a submit event listener to the form. 
  // Whenever the form is submitted, it will stop the default and then extract the values from the inputs and then update shop object. 
  // Remember, we have to prevent the default action otherwise it will get submitted to the server. 
  // Also remember that we have to use parseInt to convert the string inputted by the user into an integer otherwise the calculations will fail.
  this.bindForm = function(form, shop) {
    form.on('submit', function(e) {
      e.preventDefault();
      var minCustomers = form.find('input[name=minCustomers]').val();
      var maxCustomers = form.find('input[name=maxCustomers]').val();
      var avgDonuts = form.find('input[name=avgDonuts]').val();
      var hoursOpen = form.find('input[name=hoursOpen]').val();
      shop.minCustomersPerHour = parseInt(minCustomers);
      shop.maxCustomersPerHour = parseInt(maxCustomers);
      shop.avgDonutsPerCustomers = parseInt(avgDonuts);
      shop.hoursOpenPerDay = parseInt(hoursOpen);
      donutMaster.showShopDetails(shop);
      donutMaster.showShopForm(shop);
    })
  }
}

Step 7: Initialize the DonutMaster

Now that we have encapsulated all of our logic into classes, we can simply call a few methods to run the app.

var donutMaster = new DonutMaster();
donutMaster.addShop(new DonutShop("Downtown", 8, 43, 4.5, 8));
donutMaster.addShop(new DonutShop("Capitol Hill", 4, 37, 2, 8));
donutMaster.addShop(new DonutShop("South Lake Union", 9, 23, 6.33, 8));
donutMaster.addShop(new DonutShop("Wedgewood", 2, 28, 1.25, 8));
donutMaster.addShop(new DonutShop("Ballard", 8, 58, 3.75, 8)); 

donutMaster.generateShopList();
donutMaster.bindButtons();

Donut Shop Tutorial Part 2

In this part, we will briefly touch upon the templating pattern. These days, people use templating engines like mustache and handlebars. HTML rendering logic can end up cluttering your form/view logic so its good practice to encapsulate that into it's own class. In this tutorial, we'll create our own version of a simple DonutTemplates class to manage the HTML rendering.

Donut Templates

function DonutTemplates() {
  this.renderShopList = function(donutShops) {
    var ul = $('<ul>');
    for (var i = 0; i < donutShops.length; i++) {
      var button = $('<button>').text(donutShops[i].shopName);
      var li = $('<li>').append(button);
      ul.append(li);
    };
    return ul;
  }

  this.renderShopDetails = function(shop) {
    var div = $('<div>');
    var h2 = $('<h2>').append(shop.shopName);
    var minCustomersPerHour = $('<p>Min Customers Per Hour: </p>').append(shop.minCustomersPerHour);
    var maxCustomersPerHour = $('<p>Max Customers Per Hour: </p>').append(shop.maxCustomersPerHour);
    var avgDonutsPerCustomers = $('<p>Average Donuts Per Customer: </p>').append(shop.avgDonutsPerCustomers);
    var hoursOpenPerDay = $('<p>Hours Open Per Day: </p>').append(shop.hoursOpenPerDay);
    var donutsPerHour = $('<p>Donuts Per Hour: </p>').append(shop.donutsPerHour());
    var donutsPerDay = $('<p>Donuts Per Day: </p>').append(shop.donutsPerDay());
    div.append(h2);
    div.append(minCustomersPerHour);
    div.append(maxCustomersPerHour);
    div.append(avgDonutsPerCustomers);
    div.append(hoursOpenPerDay);
    div.append(donutsPerHour);
    div.append(donutsPerDay);
    return div;
  }

  this.renderForm = function() {
    var form = $('<form>')
    var formMinCustomerInput = $('<label name="minCustomers">Min Customers Per Hour</label><br><input type="text" name="minCustomers"><br><br>');
    var formMaxCustomerInput = $('<label name="maxCustomers">Max Customers Per Hour</label><br><input type="text" name="maxCustomers"><br><br>');
    var formAvgDonutsPerCustomer = $('<label name="avgDonuts">Avg Donuts Per Customer</label><br><input type="text" name="avgDonuts"><br><br>');
    var formHoursOpenPerDay = $('<label name="hoursOpen">Hours Open Per Day</label><br><input type="text" name="hoursOpen"><br><br>');
    var submit = $('<input type="submit" value="Recalculate">');
    form.append(formMinCustomerInput);
    form.append(formMaxCustomerInput);
    form.append(formAvgDonutsPerCustomer);
    form.append(formHoursOpenPerDay);
    form.append(submit);    
    return form;    
  }
}

Calling the templates from Donut Master

As you can see, all we've really done is move the HTML generation code from part 1 of the tutorial into its corresponding methods in the DonutTemplates class. And then we can call the methods in our DonutMaster to generate the HTML. For rendering the shop list, we just need to pass in the collection of donut shops. For rendering the shop details, we just pass in a shop object, and it will generate the shop details html and return it for us to append to the page. As for the form, we don't need to pass in anything. If you want, you can change it to accept a shop object as a parameter, and then pre-populate the form fields with the values from the shop object.

function DonutMaster() {
  this.donutShops = [];
  var nav = $('#nav');
  var navUl = $('<ul>');
  var content = $('#content');
  var donutMaster = this;
  var donutTemplates = new DonutTemplates();

  this.addShop = function(shop) {
    this.donutShops.push(shop);
  }

  this.findShopByShopName = function(shopName) {
    var shop = donutMaster.donutShops.filter(function(shop) {
      return shop.shopName == shopName;
    })[0];
    return shop;
  }

  this.generateShopList = function() {
    var ul = donutTemplates.renderShopList(this.donutShops);
    nav.append(ul);
  }

  this.bindButtons = function() {
    nav.find("button").each(function(index) {
      $(this).on('click', function(e) {
        var shopName = $(this).text();
        var shop = donutMaster.findShopByShopName(shopName);
        donutMaster.showShopDetails(shop);
        donutMaster.showShopForm(shop);
      });
    });
  }

  this.showShopDetails = function(shop) {
    var details = donutTemplates.renderShopDetails(shop);
    content.empty();
    content.append(details);
  }

  this.showShopForm = function(shop) {
    var form = donutTemplates.renderForm(shop);
    this.bindForm(form, shop);
    content.append("<h2>Edit Shop</h2>")
    content.append(form);
  }

  this.bindForm = function(form, shop) {
    form.on('submit', function(e) {
      e.preventDefault();
      var minCustomers = form.find('input[name=minCustomers]').val();
      var maxCustomers = form.find('input[name=maxCustomers]').val();
      var avgDonuts = form.find('input[name=avgDonuts]').val();
      var hoursOpen = form.find('input[name=hoursOpen]').val();
      shop.minCustomersPerHour = parseInt(minCustomers);
      shop.maxCustomersPerHour = parseInt(maxCustomers);
      shop.avgDonutsPerCustomers = parseInt(avgDonuts);
      shop.hoursOpenPerDay = parseInt(hoursOpen);
      donutMaster.showShopDetails(shop);
      donutMaster.showShopForm(shop);
    })
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment