Building a Single Page App for COVID-19 Lockdown👩🍳
Jacqueline Lam
Posted on May 14, 2020
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
};
To fetch all the matching recipes:
- Create an empty array to hold the unique
matchingRecipes
objects - Call
fetch()
and pass in a URL string to the desired data source as an argument. I'm passing in an array ofingredientIds
. -
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. - In the second
.then()
we receive a JSON string which holds thematchingRecipesData
, which we then iterate over the collection to access each recipe object. - 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. - If the fetch request is successful, the adapter method
renderMatchingRecipes(matchingRecipes)
will render all the matching recipes into the DOM. - 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
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
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:
It is important to note that there are two ways to select multiple DOM nodes:
-
document.getElementsByClassName()
- returns an
HTMLCollection
- contains same DOM elements
- returns an
-
document.querySelectorAll()
- returns a
nodeList
- can contain different DOM elements.
- can use
forEach
for iteration
- returns a
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;"));
Resources
Here are a few additional articles that are very helpful:
- Iterating over an HTML Collection in JavaScript
- Traversing the DOM with filter(), map(), and arrow functions
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:
jacqueline-lam / umami-pantry
A single page app built to help homecooks find matching recipes for limited pantry ingredients.
Posted on May 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.