DEV Community

Cover image for Building a Single Page App for COVID-19 Lockdown👩‍🍳
Jacqueline Lam
Jacqueline Lam

Posted on • Updated on

Building a Single Page App for COVID-19 Lockdown👩‍🍳

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 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.

Get Matching Recipes Data with Fetch

// 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
  };

Enter fullscreen mode Exit fullscreen mode

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:

# 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
Enter fullscreen mode Exit fullscreen mode

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


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

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:

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

Resources

Here are a few additional articles that are very helpful:

Conclusion

This is my second full-stack project (after my Rails Bolderer CMS 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 logo jacqueline-lam / umami-pantry

A single page app built to help homecooks find matching recipes for limited pantry ingredients.

Latest comments (0)