Ruby on Rails

CS 4900, Spring 2017Dr. Zhiguang Xu

Project 5: Complete the Shopping Cart
Due: February 14, 2017

In this project, we are going to complete the shopping cart (well, sort of), in two ways:

through jQuery/JavaScript (Task F in the Rails Book)
through ReactJS
Both of these two approaches use Ajax for the underlying asynchronous communications between the client and the server.

Don’t forget to git stage and commit your project often. Now let’s start…

1. Project Specifications – Part I

Complete Task F: Add a Dash of Ajax. Skip all steps pertaining to testing.

Note:

You literally have to read every single word of this Task in the Rails book.
For Iteration F1, if the color of the “Empty Cart” button at the bottom of the cart does not look right, add the following to app/assets/stylesheets/carts.scss:

.carts, #side #cart {

input {
color: buttontext;
}
}
Also for Iteration F1, you should remove the “My Cart” quick link (if you have one) from the side bar now that we are no longer displaying the cart in a separate page, therefore that quick link is no use any more.

For Iteration F2, notice that after after adding the new format js line to the create method in LineItemsController, Rails will respond to different requests in different ways:

format.html: it simply renders the entire catalog web page by sending back app/views/store/index.html.erb, including a list of books, side bar, shopping cart,…, everything.
format.js: it sends the Javascript file create.js.erb back to the client. Such a JS file will be executed by the client (i.e. browser) to render the cart only, leaving other parts of the web page untouched. A great performance booster!

Note: the erb part of that Javascript file <%=j render(@cart) %> has to be executed on the server side first though. The resulting shopping cart with the most current contents will then be sent along with the JS code back to the client.

format.json: for now, it redirects to the cart page. We will change it later in Part III of this document (specifically, section 3.2) so it renders the shopping cart as a ReactJS component while staying on the same Web page.

For Iteration F3, in order for jquery-ui to work properly, in app/assets/javascripts/application.js, change the //= require jquery-ui/effect-blind line to the following:

//= require jquery-ui/effects/effect-blind
For Iteration F4, the cart is supposed to appear smoothly in the sidebar when the first book is being added to it — a nice-to-have but non-essential feature of the project. However, such a “blind down” effect never works out consistently on all browsers for me (Firefox turns out to have the best support of jQuery UI). I am running out of time on this ??. If you can figure out how to make it work all the time across the browsers, please share it with the class.

For Iteration F5, for some reason, it turns out the app/assets/javascript/channels/products.coffee file is not being properly translated into a Javascript file, causing errors when the Depot app loads. Therefore, replace that .coffee file with the following JS file:

products.js:

(function() {
App.products = App.cable.subscriptions.create(“ProductsChannel”, {
connected: function() {},
disconnected: function() {},
received: function(data) {
$(“.store #main”).html(data.html);
}
});
}).call(this);
Both CoffeeScipt and JSX are just syntactical sugarcoats for Javascript, intended to make programmers life easier when writing Javascript (Vanilla Javascript is a VERY VERBOSE language). They both have to be compiled into Javascript before being used by the browser. Personally, I am a big fan of JSX because to me, the CoffeeScript’s syntax is simply counterintuitive and unnatural.
Still for Iteration F5, it is showcasing Action Cable, a very powerful WebSocket based real-time feature newly introduced in Rails 5. In this iteration, we truly just scratch the surface of it. For more information about Action Cable in Rails, please go to http://edgeguides.rubyonrails.org/actioncableoverview.html.

On Heroku, for a more performant handling of Action Cable/WebSocket, Redis is used. Redis is “an open source, in-memory data structure store, used as a database, cache, and messsage broker”. For more information about Redis, please go to https://redis.io/. Therefore, special steps have to be taken to make everything work when deploying your project on Heroku. See Part IV below.

2. Project Specifications – Part II

Both of the two items below are challenging yet doable. They will greatly deepen your understanding of Rails and Ajax.
Add a “-“ button next to each item in the cart. When being clicked, it should invoke an action to decrement the quantity of the item, deleting it from the cart when the quantity reaches zero, and emptying the cart when there is nothing left in the cart. Get it to work without using Ajax first, and then add the Ajax goodness.

Hint: This is a Playtime exercise at the end of Task F in the Rails book. Don’t blindly copy the code from the forum page though because they might not work completely.

When adding/removing books into/from your ajaxified shopping cart or completely emptying the cart, ajaxify the popularity values underneath the corresponding books on the catalog page to reflect such changes as well. You don’t have to re-order the books on the catalog though, unless the user refreshes the catalog him-/her-self.

3. Project Specifications – Part III

In this part, we are going to create a new ReactJS component Cart embedded inside the Catalogcomponent that we created in Project 4 (i.e Cart is a child of Catalog; or Catalog is a parent of Cart) such that a click on any “Add to Cart” button will update the contents of the shopping cart represented by this new ReactJS component while staying on the same web page.

Ideally, Cart and Catalog components should be separate and independent. However, that way, in order to be able to pass data (as props) between them and set their states, we need to use Redux — “a predictable state container for JavaScript apps” from Facebook (http://redux.js.org/). Therefore, due to time constraints, we are going to just go with the sub-optimal approach to implement the communications between Cart and Catalog components as described above (a child contained in a parent).

Also, as I described in class many many times, you will find the Chrome Developer Tools an indispensable tool when it comes to ReactJS programming. Any time you need to do some debugging, just do console.log(…);, your log message will be displayed in the console of Chrome for you to review.
3.1 Clean the SPA Catalog page

First of all, let’s hide the side column including the shopping cart and all the quick links from the SPA catalog page.

In app/controllers/store_controller.rb, in the index method, right before the render ‘index_spa’ line, add @spa = true.
Then in app/views/layouts/application.html.erb, enclose the entire division with the id of side in the following if statement such that the side column is displayed on the Non-SPA catalog page but not on the SPA catalog page:


<% if @spa.nil? %>

<% end %>

One more thing to do in order to get ready for the SPA Catalog page that will contain the shopping cart as one of its sub-components. In app/views/store/inde_spa.html.erb, replace the <%= react_component… %> line with

<%= react_component(“Catalog”, props: {catalog: @products, cart_id: @cart.id}, prerender: false) %>
Basically, we pass the id of the current cart as yet another props to the Catalog component such that it knows which cart to render.

3.2 Get the Rails server ready

When we click on an “Add to Cart” button on the SPA catalog page powered by ReactJS, we want the Rails server to deal with such a request in the same way as it does with any other request, except that at the end, it needs to return the updated shopping cart in JSON format.

Remember in Project 4 (section 2.3.2), we did something similar to this in the index method of StoreController (i.e., the format.json {render json: Product…} line). However, at that time, it was quite simple because we were dealing with one single model of Product only. Now, the data that we need to collect from the database, put in the desired format, and send back to the client involve three associated models: Cart, LineItem, and Product. If we stick with the same approach as in project 4, the format.json {…} line in LineItemsController can grow very complicated before you realize it.??

JBuilder to the rescue!

JBuidler gives you a simple DSL for declaring JSON structures that beats manipulating giant hash structures. It used to be an optional gem in the previous version of Rails. Now, Rails 5 includes it by default. For more information about this gem that comes handy, especially for us having to feed ReactJS components with multiple JSON data, please go to https://github.com/rails/jbuilder.
In the app/views/line_items folder, add a new file create.json.jbuilder:

json.line_items @cart.line_items do |line_item|
json.id line_item.id
json.quantity line_item.quantity
json.title line_item.product.title
json.total_price line_item.total_price
if (line_item == @line_item )
json.current_item true
end
end

json.total_price @cart.total_price
Notice that we structure the JSON data here in the same way as how we want them to be displayed in the shopping cart. An alternative way is to just send back the raw data and let ReactJS worry about the structure of the cart.
In the create method of LineItemsController, replace the entire format.json {…} line with the following. By default, Rails will render the data as specified in the .jbuilder file above and return it back to the client.


format.json { }

In the show method of CartsController, add the following:


respond_to do |format|
format.html
format.json
end

As we just learned, for the second format (json) above, Rails will automatically render the file app/views/carts/show.json.jbuilder. This file was pre-created for you. Now, you need to replace its current contents with the same code as we did in app/views/line_items/create.json.jbuilder above.

3.3 Componentize the Shopping Cart with ReactJS

Our experience with ReactJS so far teaches us how to modify the state of a parent component from a child component through the state-modifying function passed from the parent down to the child as a props.

E.g. In SortColumn, whenever one of the table headers (i.e. title, price, or popularity) is clicked, the local function handleSort is called, which in turn calls this.props.handleSortColumn. Such a function was passed down from Catalog via BookList to SortColumn as one of the props. It is handleSortColumn in Catalog that actually fetches data from the server according to the new sorting criterion and updates the state of Catalog once the data comes back.

Similarly, in project 4, as soon as one of the “Add to Cart” buttons is clicked, via a chain of props from Book to BookList to Catalog, a function handleAddToCart in Catalog is called. Now, we need to be able to do the opposite from what we learned before — we need to update the state of the child Cart from the parent Catalog. In ReactJS, this is done through refs (or references). For more information, go to https://facebook.github.io/react/docs/refs-and-the-dom.html.

Modify the Catalog component in the following way so that instead of going to a different page display the shopping cart, we stay on the same SPA page:

At the beginning, add

import Cart from ‘./Cart’;
In the render function, add one new div to render the Cart component, which will be defined later. Cart has two properties: ref=”cart” which will be used by the parent component Catalog to hook to its child component Cart (see next step); and id={this.props.cart_id} as a props whose value comes from the last step in section 3.1 above.


render() {
return (



);
}

Then in the handleAddToCart function, replace the window.location = response.headers.location; line with the following:


self.refs.cart.handleAddToCart(response.data);

Notice response.data above? It is the updated shopping cart with new contents that we put together in the format of JSON in section 3.2.

One last step before writing the Cart component itself: add the following CSS description in app/assets/stylesheets/store.scss such that the cart on the SPA catalog page appears to be the same as the one on the regular non-SPA page:

.store {
.carts {
text-align: right;

.item_price, .total_line {
text-align: right;
}

.total_line .total_cell {
font-weight: bold;
border-top: 1px solid #595;
}

input {
color: buttontext;
}
}


}
Finally, you need to put together three ReactJS components — Cart, LineItems, and LineItem. As you can see, this step tests if you can wrap your head around ReactJS.

I am giving you skeleton code for these three .jsx files in Appendix A. Notice that Cart contains LineItems. LineItems contains LineItem. Fill them up with your own code.
Note, in jsx, any time you need specify a CSS class, you need to say className because class is already a reserved word.
Use Catalog, BookList, and Book as your references when writing your components.
At the beginning, worry neither about the ‘-‘ buttons at the end of line items rows in the cart, nor the “Empty Cart” button. Make sure that the rest of the cart works first.
Finally, implement the ‘-‘ and ‘Empty Cart’ buttons. In order for them to work, on the server side, you need to
Modify the decrement method in LineItemsController. Add decrement.json.jbuilder correspondingly.
Modify the destroy method in CartsController. Add destroy.json.jbuilder correspondingly.
In order to focus on componentizing the shopping cart, you don’t have to worry about updating the popularity values of the books dynamically as specified in Part II of this document.
4. Project Specifications – Part IV

In order for the Action Cable/WebSocket part (i.e. Task F, Iteration F5) of the Depot project to work on Heroku, in addition to the regular steps for project deployment, you need to do some extra configurations as listed below:

Add the following gem to your Gemfile, then run bundle install.

gem ‘redis’, ‘~>3.2’
In config/cable.yml, update the production clause:

production:
adapter: redis
url: <%=ENV[‘REDISTOGO_URL’]%>
The environment variable REDISTOGO_URL on Heroku will be setup later. It is going to be the URL address of the Redis server running Heroku.

In config/environments/production.rb, mount Action Cable by uncommenting the following two lines (make sure you replace the project name with yours though)

config.action_cable.url = ‘wss://project5-yourname.herokuapp.com/cable’
config.action_cable.allowed_request_origins = [ ‘http://project5-yourname.herokuapp.com/’]
In config/routes.rb, uncomment the following line:

mount ActionCable.server => ‘/cable’
Git stage and commit all the configurations above. Push your project to Heroku.

Run the following command to install redistogo as a Heroku addon. It also sets up the environment variable REDISTOGO_URL for you. You may run heroku configure to verify it.

heroku addons:add redistogo
Reset, create, and populate the database tables on Heroku.

Rename your application to project5-yourname and it should be good to go.

5. What to Turn in?

Final version of the project source code pushed onto Github

Alert: Don’t push your P5 to the master branch onto your Github repository until I explicitly tell you so on BlazeVIEW. I need to pull your Project 4 down for grading purposes first before you overwrite it with the code for this Project 5.

Final version of the project deployed on Heroku. You must name your application on Heroku:

http://project5-yourfirstname-yourlastname.herokuapp.com
The Git log file reflecting the full history of your project submitted on BlazeVIEW

Appendix A

Cart.jsx:

import React from ‘react’;
import LineItems from ‘./LineItems’;
import axios from ‘axios’;

const Cart = React.createClass ({
getInitialState: function() {
return {
// states of Cart
};
},

componentDidMount: function() {
// send an HTTP get message to
// request json data from the server at the url
// ‘/carts/’+this.props.id
// and update the states with it
},

handleRemoveFromCart: function(id){
// send an HTTP patch message to
// request json data from the server at the url
// ‘/line_items/’+id+’/decrement’
// and update the states with it
},

handleEmptyCart: function(){
// send an HTTP delete message to
// request json data from the server at the url
// ‘/carts/’+this.props.id
// and update the states with it
},

handleAddToCart: function(cart){
// update the states with “cart”
// that comes from the line “self.refs.cart.handleAddToCart(response.data);”
// in the “Cart” component
},

render: function() {
if (this.state.total_price != 0) {
return(

// render the LineItems component here

)
}
else {
return (

Your Cart

);
}
}
});
export default Cart;

LineItems.jsx:

import React from ‘react’;
import LineItem from ‘./LineItem’;

const LineItems = React.createClass ({

handleRemoveFromCart: function(id) {
// call handleRemoveFromCart in Cart to handle it
},

render: function() {
// populate an array “line_items” with
// a collection of LineItem components

return(

// render line items // render the total price line

)
}
});

export default LineItems;
LineItem.jsx:

import React from ‘react’;

const LineItem = React.createClass ({
propTypes: {
// types of props
},

handleRemoveFromCart: function(e) {
// call handleRemoveFromCart in LineItems to handle it
},

render: function() {

return(

// render a line item row

)
}
});

export default LineItem;

Leave a Reply

Your email address will not be published. Required fields are marked *