Building a Single Page App for COVID-19 Lockdown👩‍🍳
Tags: javascript, rails, ruby, webdev
Published: 2020-05-14 07:40:33

## Umami Pantry
### A Single Page App with Javascript/ Rails API
Since we are all in lockdown and the grocery stores are packed with people these days, I have created a single page application called **[Umami Pantry](https://github.com/jacqueline-lam/umami-pantry)** to help users find matching recipes for available ingredients in their kitchen. It is designed to encourage freestyle cooking with easy-to-substitute ingredients.

The app is composed of backend Rails API and front-end modular JS clients, which use asynchronous Javascript to make HTTP requests to the API to get/ post data and render them to the user interface.


### Client-Server Communication
All the interactions between the client and the server are handled asynchronously with the `fetch()` method provided by [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). 

#### Get Matching Recipes Data with Fetch

```javascript
// Adapter class
  getMatchingRecipes() {
    let matchingRecipes = [] // 1
    // 2, 3, 4
    return fetch(`http:localhost3000/get_recipes/?selected_ingredients=${this.selectedIngredients}`)
      .then(resp => resp.json())
      .then(recipesData => { 
        recipesData.forEach(recipe => {
          // 5
          let r = Recipe.findById(recipe.id)
          r = r || new Recipe(recipe)
          matchingRecipes.push(r);
        })
        this.renderMatchingRecipes(matchingRecipes); // 6
      })
      .catch(err => console.log(err)); // 7
  };

```
To fetch all the matching recipes: 
1. Create an empty array to hold the unique `matchingRecipes` objects
2. Call `fetch()` and pass in a URL string to the desired data source as an argument. I'm passing in an array of `ingredientIds`.
3. `fetch()` returns an object representing the data source sent back (not the actual JSON). We then call `.then()` on this object, which accepts the callback function, receiving the response as its argument and call the `.json()` method to return the content from the response.  
4. In the second `.then()` we receive a JSON string which holds the `matchingRecipesData`, which we then iterate over the collection to access each recipe object.
5. Search for the recipe in the Recipe class, if the recipe object does not exist, instantiate a new Recipe object. Push the recipe object into the `matchingRecipes` array.
6. If the fetch request is successful, the adapter method `renderMatchingRecipes(matchingRecipes)` will render all the matching recipes into the DOM.
7. Add a `.catch()` after the two `.then()` calls, appending an error message to the console if `.catch()` is called.


--------------------------------------------
#### Render JSON from a Rails Controller
Between step 2 and 3, we use the `/get_recipes` endpoint to access the matching pieces of recipe data. We get the matching instances in the Recipe model and render them into JSON in the recipes controller:
```ruby
# Step 2.5
class RecipesController < ApplicationController
  def get_recipes
    selected_ingredients = params[:selected_ingredients].split(',').map(&:to_i)
    recipes = Recipe.filter_by_ingredients(selected_ingredients)
    render json: RecipeSerializer.new(recipes).instances_to_serialized_json
  end
end
```
We first extract the the string of `ingredientIds` from the params and convert them into a string of intergers. We then filter out the Recipe instances that include the specific set of ingredients.

We call `render json:` followed by the customized data that would be converted to JSON. The customized data is handled by the `RecipeSerializer` service class, which handles the logic of extracting and arranging the JSON data that we want to send back to the client. 

--------------------------------------------
#### Results
![Matching recipe results](https://dev-to-uploads.s3.amazonaws.com/i/b5qfboedkhw8q6zrlqd6.png)

--------------------------------------------
### Iterations in JavaScript
There are a lot of ways to traverse a collection in Javascript. However, it can get quite confusing especially when you want to iterate through Array-like DOM objects. There are `.map`, `for..in`, `for...of` and `.forEach` but they are all slightly different.

For example, using a `.forEach` method on an `HTMLcollection` would cause a TypeError:
![TypeError in attempt to iterate through HTMLColelction](https://dev-to-uploads.s3.amazonaws.com/i/5pobvfxbvqxrn47i5m40.png)

It is important to note that there are two ways to select multiple DOM nodes: 

1. `document.getElementsByClassName()` 
  * returns an `HTMLCollection`
  * contains same DOM elements

2. `document.querySelectorAll()`
  * returns a `nodeList`
  * can contain different DOM elements.
  * can use `forEach` for iteration

To iterate over the `HTMLcollection`, we can use `Array.from()` to convert the HTML collection into an array and then traverse the collection like an array with the `.forEach` method:

```javascript
const ingredientCards = document.getElementsByClassName('ingredientCard');
Array.from(ingredientCards).forEach(card => card.setAttribute("style", "background-color: white;"));
```

#### Resources
Here are a few additional articles that are very helpful: 
* [Iterating over an HTML Collection in JavaScript](https://medium.com/@larry.sassainsworth/iterating-over-an-html-collection-in-javascript-5071f58fad6b)
* [Traversing the DOM with filter(), map(), and arrow functions](chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#ttl=Traversing%20the%20DOM%20with%20filter()%2C%20map()%2C%20and%20arrow%20functions&pos=4022.5&uri=https://medium.com/@chuckdries/traversing-the-dom-with-filter-map-and-arrow-functions-1417d326d2bc)

### Conclusion
This is my second full-stack project (after my [Rails Bolderer CMS App](https://github.com/jacqueline-lam/rails-bolderer-app)), and I am glad that I am able to focus more on the front-end for this project. Learning JavaScript is a breath of fresh air, and I am looking forward to learning more efficient ways to manipulate the DOM, make better use of `eventListeners` to create more interactive and responsive sites, and to communicate with the server asynchronously.

Please feel free to check out my project and leave any feedback below:
{% github jacqueline-lam/umami-pantry no-readme %}