Build a Recipe App With Node.js & PostgreSQL - Edit & Delete Recipe Part 5

Build a Recipe App With Node.js & PostgreSQL - Edit & Delete Recipe Part 5

This is the fifth part of the Build a Recipe App With Node.js & PostgreSQL series. In this part, we will be implementing the code edit and delete recipes.

Parts

Adding database queries

Add the following queries to queries.js

Removing recipe

async function removeRecipe(id) {
  const query = await db.query('DELETE FROM recipe WHERE id = $1;', [id]);

  return query.rows;
}

This will remove a recipe with a matching id.

Removing recipeIngredients

async function removeRecipeIngredients(recipeId) {
  const query = await db.query('DELETE FROM recipe_ingredient WHERE recipe_id = $1', [recipeId]);

  return query;
}

This will remove all recipeIngredients with a matching recipe id. Remember this is the table that connects everything with a recipe.

Update recipe

async function updateRecipe(id, values) {
  const query = await db.query(
    'UPDATE recipe SET name=$1, description=$2, instructions=$3, image=COALESCE($4, image) WHERE id=$5 RETURNING *',
    [...values, id]
  );

  return query.rows;
}

This will try to find a recipe by id and update it. By using COALESCE if a null value is passed we just keep the old path of the image.

Don't forget to export the functions at the end.

module.exports = {
  getOneRecipe,
  getAllRecipes,
  updateRecipe,
  getAllMeasures,
  insertIngredients,
  getIngredients,
  insertRecipeIngredients,
  removeRecipeIngredients,
  insertRecipe,
  removeRecipe,
};

Adding new routes

Update app.js with the following code.

app.delete('/recipes/:id', async (req, res) => {
  queries.removeRecipe(req.params.id);
  res.redirect('/');
});

This route accepts a recipe id and will remove the recipe after that redirect to the front page.

app.get('/recipes/:id/edit', async (req, res) => {
  const recipe = await queries.getOneRecipe(req.params.id);
  const measurements = await queries.getAllMeasures();

  res.render('editRecipe', { recipe, measurements });
});

This route will be used to the form to edit/delete the recipe with all previous data prefilled.

app.put('/recipes/:id', upload.single('image'), async (req, res) => {
  const { ingredients, measures, amounts, name, description, instructions } = req.body;
  const filename = req.file ? req.file.filename : null;

  await queries.insertIngredients(ingredients);
  const allIngredients = await queries.getIngredients(ingredients);
  const ingredientsIds = allIngredients.map((ingredient) => ingredient.id);

  await queries.removeRecipeIngredients(req.params.id);
  await queries.insertRecipeIngredients(
    new Array(ingredients.length).fill(req.params.id),
    ingredientsIds,
    measures,
    amounts
  );

  await queries.updateRecipe(req.params.id, [name, description, instructions, filename]);

  res.redirect('/');
});

This one is very similar to adding a new recipe with some minor differences. We start by destructuring all data from the body and then we check if a new file was added else keep the old one by setting filename to null (updateRecipe will ignore it). After that, we insert ingredients in case any new was added that was not previously in the database. Next, we retrieve all ids for the ingredients.

We then remove all ingredients connections to the recipe and insert them again. This will make sure the data is correct no matter if the user added or removed any ingredient.

After that, we update the recipe with name, description, instructions and filename. In case no data was changed recipe will be the same as before.

Adding view to display edit form of a recipe

Create a file called editRecipe.handlebars and add it to the views folder with following content.

<div class="container space-top">
    <div class="row">
        <form method="POST" action="/recipes/{{recipe.id}}?_method=PUT" enctype="multipart/form-data"
            class="col sm12 l10 offset-l1">
            <div class="row">
                <div class="input-field col s12">
                    <input required name="name" id="name" type="text" value="{{recipe.name}}" class="validate">
                    <label for="last_name">Name</label>
                </div>
            </div>
            <div class="row">
                <div class="input-field col s12">
                    <textarea name="description" id="textarea1"
                        class="materialize-textarea">{{recipe.description}}</textarea>
                    <label for="password">Description</label>
                </div>
            </div>
            <div class="row">
                <div class="input-field col s12">
                    <textarea required id="textarea" name="instructions"
                        class="materialize-textarea">{{recipe.instructions}}</textarea>
                    <label for="textarea">Instructions</label>
                </div>
            </div>
            <div id="inputs">
                {{#each recipe.ingredients}}
                <div class="row">
                    <div class="input-field col s4">
                        <input required id="ingredient" value="{{this.ingredient}}" name="ingredients[{{@index}}]"
                            type="text">
                        <label for="ingredient">Ingredient</label>
                    </div>
                    <div class="input-field col s4">
                        <select required name="measures[{{@index}}]">
                            {{#each @root.measurements}}
                            <option {{#if (eq this.id ../this.measure.id)}} selected {{/if}} value="{{default this.id}}">{{this.name}}</option>
                            {{/each}}
                        </select>
                        <label>Measure</label>
                    </div>
                    <div class="input-field col s4">
                        <input required value="{{this.amount}}" name="amounts[{{@index}}]" id="amount" type="number">
                        <label for="amount">amount</label>
                    </div>
                </div>
                {{/each}}
            </div>
             <div style="text-align: right;">
                <a id="addBtn" class="btn-floating btn-large waves-effect waves-light"><i
                        class="material-icons">add</i></a>
                <a id="removeBtn" class="btn-floating btn-large waves-effect waves-light red"><i
                        class="material-icons">delete</i></a>
            </div>
            <div class="row">
                <div class="file-field input-field s12">
                    <div class="btn">
                        <span>File</span>
                        <input type="file" name="image">
                    </div>
                    <div class="file-path-wrapper">
                        <input class="file-path validate" type="text" placeholder="Upload one or more files">
                    </div>
                </div>
            </div>
            <div style="text-align: right;">

                <button class="btn waves-effect waves-light" type="submit" name="action">Submit
                    <i class="material-icons right">send</i>
                </button>
            </div>
        </form>
    </div>
</div>

This view is very similar to the view for creating a new recipe. The big differences are this view will prefill all inputs with the current data of the recipe and send the request to another endpoint.

if you now visit http://localhost:3000 you should be able to click on a recipe and then edit the data or remove the recipe.

Conlusion

In this part, we finished our recipe app by adding edit and delete functionality. I hope you enjoyed this series.

Final code