Rails/JS Project: Apple Expert Front End
Tags: webdev, javascript, html, css
Published: 2021-08-23 15:53:27

*See my previous post about setting up the back end <a href="https://dev.to/jrrohrer/rails-js-project-apple-expert-back-end-3bio">here</a>.*

When I started the front end for this project, I sketched out a basic idea of what I wanted. A hero header image followed by a drop-down where a user can select the type of recipe they're making, then a submit button. Below that would be a div that is hidden until it is populated with cards displaying each apple that matches their query. At the bottom of the page would be a hidden form for adding a new apple to the database. When a button is clicked, the form would appear. When the form is submitted, the user will see a preview of their new apple card and a success message. 

I started with the bones of the app: the HTML. I hashed out the required elements with descriptive id's so that they can easily be found and manipulated with css and javascript later. I created the forms but left the drop down select elements empty so that they can be dynamically populated with javascript. I then created the needed javascript files and linked them at the bottom of the html body section. If I add a footer later, I will move the links down so that all of the html content is loaded before the JS files are run.

With the HTML finished for now, I moved on to the index.js page. Here I made a mistake and decided to just start hashing out my functionality in vanilla js. The project requirements say that my JS has to be object oriented according to ES6 standards, and I should have written it that way from the start, because refactoring later was a headache.

Once I got most of the behavior I wanted on the DOM, I made a new branch in git and started refactoring. I have 5 javascript files. 

```
index.js // handles all of the direct DOM manipulation/event listeners
apple.js // the apple class that handles instances of apple objects
category.js // the category class that handles instances of category objects
appleApi.js // handles api calls to the apples endpoint
categoryApi.js // handles api calls to the category endpoint
```
Refactoring involved me moving code from index.js into the appropriate class until all that remained in index.js is the javascript that specifically controls the DOM in a way that is not directly related to one of the classes. All of my global variables live here, along with some event listeners, and a form handler function that takes the user's input in the create apple form and turns it into data that can be passed back to the database in a post fetch. 

The order of operations goes like this:
On page load, the drop down select elements are populated with the category objects:

```
// categoryApi.js
getCategories() {
  return fetch(this.baseUrl)
    .then(response => response.json());
}

populateCategoryDropdown() {
    this.getCategories()
    .then(categories => {
      const dropDown = document.getElementById('category'); // gets the first select element
      const categorySelection = document.getElementById('add-category'); // gets the select element in the new apple form
      categories.data.forEach(category => { // for each category in the data object
        let c = new Category(category.id, category.attributes)
        let newOption = new Option(c.name, c.id) // create a new option with name key and id value
        dropDown.add(newOption, undefined) // add the new option to the bottom of the dropdown list
        let newCategoryOption = new Option(c.name, c.id)
        categorySelection.add(newCategoryOption, undefined) // does the same thing, but for the create new apple form at the bottom of the page
      })
    })
    .catch(err => alert(err));
  }
```
Then, when the user selects the category and clicks "Show me the apples!", a second get fetch is sent, this time to the apple endpoint, with a query param that contains the category id for the selected category. The back end sends back only the apples that match that category. The apple data is iterated over, and a new apple object is created for each apple in the data object. Then, each apple has a card created for it and displayed on the DOM.

```
// appleApi.js
  getApples() {
    let categoryId = parseInt(document.getElementById('category').value); // getting the category ID and turning it into an integer
    fetch(`${port}/apples?category_id=${categoryId}`)
    .then(response => response.json())
    .then(apples => {
      appleCardsContainer.innerHTML = ""; // clears any old search results before displaying new ones
      messageDiv.innerHTML = ""; // clears any messages before new search
      Apple.all = []; // clears the Apple.all array before handling the new search results
      apples.data.forEach(apple => {
        let a = new Apple(apple.id, apple.attributes)
        a.displayApple()
      })
    })
    .catch(err => alert(err));
  }
```

```
// apple.js

getCard() {
  const appleCard = `
    <div data-id=${this.id} class="apple-card">
      <img src="${this.image_url}">
      <h3>${this.variety}</h3>
      <h4>Harvested in ${this.harvest}</h4>
      <p>${this.notes}</p>
      <button>Delete</button>
    </div>`;

  this.element.innerHTML = appleCard;
  return this.element;
}

displayApple = () => {
  const appleCard = this.getCard();
  Apple.container.appendChild(appleCard);
}
```

If the user decides to create a new apple, they can click the button to show the form, then fill it out. When they click submit, we prevent the default post action and instead collect the user's inputs and pass them to a post fetch back to the apple endpoint.

```
// index.js

function createFormHandler(e) {
  e.preventDefault();
  const varietyInput = document.querySelector("#input-variety").value;
  const harvestInput = document.querySelector("#input-harvest").value;
  const notesInput = document.querySelector("#input-notes").value;
  const imageInput = document.querySelector("#input-image-url").value;
  const categorySelections = document.getElementById('add-category').selectedOptions;
  // the following line collects all of the ids from the selected category objects
  const categoryIds = Array.from(categorySelections).map(x => x.value);
  appleApi.postApple(varietyInput, harvestInput, notesInput, imageInput, categoryIds)
  e.target.reset()
}
```

The post fetch is then called:

```
// appleApi.js

postApple(variety, harvest, notes, image_url, category_ids) {
  let bodyData = {variety, harvest, notes, image_url, category_ids}
  fetch(`${port}/apples`, {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({apple: bodyData})
  })
  .then(response => response.json())
  .then(apple => {
    appleCardsContainer.innerHTML = ""; // clears previous results
    messageDiv.innerHTML = "<h3>Your apple has been saved to the database and will be included in future searches. Thanks for contributing to Apple Expert!</h3>" // displays success message
    let a = new Apple(apple.data.id, apple.data.attributes) // creates new apple object
    a.displayApple() // displays new apple card on the DOM
  }) 
  .catch(err => alert(err));
}
```

When the user searches again, the previous results are cleared, as well as the success message. The new apples will appear in any new searches. 

The final feature I added was the ability to delete the apples, both from the DOM and the database. I added a delete button to each apple card, created a function to clear the card from the DOM, and then destroy the apple object in the database.

```
// apple.js

// added these two lines to the apple constructor:
this.element = document.createElement('div');
this.element.addEventListener('click', this.handleDeleteClick)

handleDeleteClick = (e) => {
  if(e.target.innerText === "Delete"){
    this.deleteApple(e)
  }
}

// appleApi.js

deleteApple = (id) => {
  fetch(`${port}/apples/${id}`, {
      method: "DELETE",
      headers: {"Content-Type": "application/json"},
  })
    .then(response => response.json())
    .then(json => alert(json.message)) 
}
```
Once the apple is deleted, the user will see an alert modal notifying them that the delete was successful.

The final thing I worked on was the CSS. I linked a stylesheet to the index.html file and hashed out a css grid for the apple cards, handled hiding empty divs, controlled image sizing so that the images on the cards are roughly the same size but retain their aspect ratio, dressed up the buttons and forms, and added some other decorative touches.

This was my first project that incorporated using JavaScript and using Rails to set up an API. It took me longer than any of the other projects to do, and when I explained it to my mom, she said, "That doesn't sound so hard." Heh. Granted, it's no Facebook or anything, but this project was challenging for me, especially since trying to force JavaScript into my brain was like mixing oil and water. There are still lots of things I don't exactly understand, but practice and reviewing will get me there.

Here's the repo: https://github.com/jrrohrer/apple-expert-frontend