Learning Rails – Meal Plan UI (Part 6)

Thu Dec 15, 2016 - 2100 Words

Today we’re going to build on the meal plan functionality from the previous tutorial and create the UI for a user to view and create new meal plans.

Goal for this Tutorial:

  • Display a Meal Plan.
  • Allow a User to create a meal plan.

The ability to have a MealPlan pre-fill its own meals took out the most complicated portion of a meal plan, and now we get to put a face on that functionality. Each MealPlan is not set to be specific length so we need to have a fairly dynamic user interface that can handle multiple lengths.

Here’s how I imagine we can work through creating a meal plan:

1) Specify the meal plan start and end dates (default to today, and six days from now). Submit the form. 2) Return a form that has a pre-filled Meal for each date in the date range. 3) Allow user to customize the recipe in each meal before creating the MealPlan.

After the MealPlan has been created we will allow people to delete the plan and edit the recipes. We’ll use a _form partial to handle the new/edit views since they’ll be of dynamic length based on the meal plan duration. We’ll be creating all of the CRUD actions for MealPlan in the same way that we did for Recipe a few tutorials back.

Creating a New Meal Plan

The first thing we’re going to work on will be the initial creation of a MealPlan. The previous tutorial took us through flushing out a simple plan before we ever display it to the user, and now we get to utilize that.

To start we need to create our routes and controller.

config/routes

resources :meal_plans

app/controllers/meal_plans_controller.rb

class MealPlansController < ApplicationController
  before_action :require_login

  def new
    @meal_plan = current_user.meal_plans.build(
      start_date: params[:start_date] || Date.today,
      end_date: params[:end_date] || 6.days.from_now.to_date
    )

    @meal_plan.build_meals
  end
end

We learned last time that we need to remember to require a user to be logged in for them to be able to work with things such as recipes or meal plans.

By default we’re going to create meal plans that are for a whole week, and before we render the view we want to call build_meals so that we have some pre-populated Meal objects.

The view for creating a new MealPlan will actually be a little different than what we did for Recipe, it won’t be the same view that we render for edit later on. We will actually have two forms on the page:

  1. A form to select the date range. Submitting this will re-render the new meal plan page with the proper number of meals.
  2. The Meal Plan form that allows you to customize the meals that were pre-populated.

New Meal Plan Forms

Let’s build write out the date range form first.

app/views/meal_plans/new.html.erb

<h1>New Meal Plan</h1>

<%= form_for @meal_plan, url: new_meal_plan_path, method: "GET" do |form| %>
  <div class="form-control">
    <%= form.label :start_date %>
    <%= form.text_field :start_date, type: "date" %>
  </div>

  <div class="form-control">
    <%= form.label :end_date %>
    <%= form.text_field :end_date, type: "date" %>
  </div>

  <div class="action">
    <%= form.submit "Generate Plan" %>
  </div>
<% end %>

<%= render partial: "form" %>

Notice that this is a pretty standard form other than the fact that we’re defining a url and method in our form_for call. We’re doing this because by default it would create a form that pointed to the “create” action in our controller and use the “POST” method. We want this to send our new dates to the “new” action and re render the page so we had to customize this a bit.

Next, we’ll create the form for customizing the meals. Since we know we’ll be working with this portion of the page in the “edit” flow we’ll go ahead and put it in the _form.html.erb partial now.

app/views/meal_plans/_form.html.erb

<% if @errors.present? %>
  <div class="errors">
    <ul>
      <% @errors.each do |error| %>
        <li><%= error %></li>
      <% end %>
    </ul>
  </div>
<% end %>

<%= form_for @meal_plan do |form| %>
  <%= form.hidden_field :start_date %>
  <%= form.hidden_field :end_date %>

  <div class="meals">
    <%= form.fields_for :meals do |meal_fields| %>
      <div class="meal">
        <%= meal_fields.hidden_field :id %>
        <%= meal_fields.hidden_field :date %>
        <p><%= meal_fields.object.date.to_s %></p>
        <div class="form-control">
          <%= meal_fields.label :recipe_id %>
          <%= meal_fields.select :recipe_id, current_user.recipe_options %>
        </div>
      </div>
    <% end %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

This form is quite a bit more complicated than our previous forms. We need to store off a good deal of information in hidden_fields, but the really interesting part is the form.fields_for :meals block. fields_for let’s us iterate through each of the Meal objects that we’ve created. meal_fields is an object that works just like our original form builder (form) except that it sets up the name for each field a little differently. We have hidden fields for the id of both the MealPlan and each Meal because we will need those when we get around to allowing a meal plan to be edited.

We are calling a method on current_user that doesn’t actually exist yet that will render out the options for the select box. Select fields in rails expect to receive and array of arrays in the following shape:

[
  ["Displayed Name 1", "hidden_value1"],
  ["Displayed Name 2", "hidden_value2"],
]

To do this with the user’s Recipe objects we’ll need to return the recipe name and the recipe id. Let’s add that method to User now.

app/models/user.rb

def recipe_options
  recipes.map do |recipe|
    [recipe.name, recipe.id]
  end
end

Meal Plan Create Action

Now that we have our forms we should be able to actually generate the MealPlan. Let’s create our create and meal_plan_params methods:

app/controllers/meal_plans_controller.rb

def create
  @meal_plan = current_user.meal_plans.build(meal_plan_params)

  if @meal_plan.save
    redirect_to meal_plan_path(@meal_plan), notice: "Meal plan created!"
  else
    @errors = @meal_plan.errors.full_messages
    render :new
  end
end

private

def meal_plan_params
  params.require(:meal_plan).permit(
    :start_date,
    :end_date,
    meals_attributes: [
      :id,
      :date,
      :recipe_id
    ]
  )
end

There really isn’t a whole lot going on in the create action that we haven’t seen before, but meal_plan_params does look a different. Since we used fields_for in our form we’re going to be receiving an array of Meal object fields when the form is submitted and we need to allow those to come through the strong parameters. We’re not quite done though, because the MealPlan model doesn’t know that it should be on the look out for meals_attributes. We need to add a line to MealPlan before we continue.

app/models/meal_plan.rb

accepts_nested_attributes_for :meals

If you submit the form now, you won’t see an error, but the form will re-render. This happens because our Meal objects can’t save. This happens because of the we’re not associating the uncreated MealPlan to the uncreated Meal. We can hook this up by adding another key to the has_many and belongs_to lines of our models:

app/models/meal_plan.rb

has_many :meals, -> { order(:date) }, inverse_of: :meal_plan

And in the Meal model:

app/models/meal.rb

belongs_to :meal, inverse_of: :meals

Now if we submit our form we should successfully create our meal plan and receive the error that we don’t have a show action. Before we build show though let’s clean up our view a little by removing the duplication in our application when it comes to rendering errors.

DRYing Up Form Errors

Our form partial and the recipe form share the same common error displaying markup and logic. We can extract that out so that when we change it both of the forms will be updated. Let’s create a subdirectory in views to hold our shared views (called shared)

$ mkdir -p app/views/shared

Now we’ll create a new partial called _errors.html.erb.

app/views/shared/_errors.html.erb

<% if @errors.present? %>
  <div class="errors">
    <ul>
      <% @errors.each do |error| %>
        <li><%= error %></li>
      <% end %>
    </ul>
  </div>
<% end %>

Now in the meal plan and recipe _form partials replace the error rendering block with a new render partial call.

<%= render partial: "shared/errors" %>

Showing & Listing Meal Plans

Displaying our meal plan won’t be too different from what we’ve done in the past. Here’s the show action:

app/controllers/meal_plans_controller.rb

def show
  @meal_plan = current_user.meal_plans.find(params[:id])
end

Here’s the corresponding view:

app/views/meal_plans/show.html.erb

<h1>Meal Plan: <%= @meal_plan %></h1>

<h2>Meals</h2>

<ul class="meals">
  <% @meal_plan.meals.each do |meal| %>
    <li class="meal">
      <%= meal.date %> - <%= link_to meal.recipe, recipe_path(meal.recipe) %>
    </li>
  <% end %>
</ul>

<div class="actions">
  <ul class="recipe-actions">
    <li>
      <%= link_to "Edit Meal Plan", edit_meal_plan_path(@meal_plan) %>
    </li>
    <li><%= link_to "Delete", meal_plan_path(@meal_plan), data: {
      confirm: "Are you sure you want to delete meal plan: #{@meal_plan}?",
    }, method: :delete %></li>
  </ul>
<div>

We’re trying to create a link to a meal plan by passing it in as the first argument to link_to. From a programmer point of view this is great, but unfortunately that won’t quite work as expected out of the gate. link_to is going to look for the String representation of the first argument and render that. With that knowledge in mind we can customize MealPlan and the way that it displays itself as a String.

app/models/meal_plan.rb

def to_s
  "#{start_date} - #{end_date}"
end

Now when we pass a MealPlan object into a helper that renders a string or simple render it to the page using a <%= meal_plan %> we will see a human readable start date and end date.

We will also define to_s on Recipe so that it prints out the name.

app/models/recipe.rb

def to_s
  name
end

Listing Meal Plans

Before we go about listing out meal plans we should think about what makes that index view for meal plans useful. When a user goes to this page what they probably want to see is his/her previous weeks, but more importantly the currently active plan. Because of this we’re not simply going to return a list to the view to have it rendered.

Here’s the index action:

app/controllers/meal_plans_controller.rb

def index
  @current_meal_plan = current_user.meal_plans.where("start_date <= ? AND end_date >= ?", Date.today, Date.today).first
  @meal_plans = current_user.meal_plans.order("start_date desc")
end

We need to use a custom SQL query in our where statement to ensure that we’re finding the currently active meal plan (or one of them). As for the rest of the @meal_plans we just want to make sure that we’re showing the most recent plans first.

Let’s take a look at the view that will display the plans.

app/views/meal_plans/index.html.erb

<h1>Meal Plans</h1>

<% if @current_meal_plan.present? %>
  <h2>Current Meal Plan</h2>

  <div>
    <ul class="meals">
      <% @current_meal_plan.meals.each do |meal| %>
        <li class="meal">
          <%= meal.date %> - <%= link_to meal.recipe.to_s, recipe_path(meal.recipe) %>
        </li>
      <% end %>
    </ul>
  </div>
<% end %>

<h3>All Meal Plans</h3>

<% @meal_plans.each do |meal_plan| %>
  <div class="meal-plan">
    <%= link_to meal_plan, meal_plan_path(meal_plan) %>
  </div>
<% end %>

<div class="actions">
  <%= link_to "New Meal Plan", new_meal_plan_path %>
</div>

Deleting a Meal Plan

Similarly to how we allow a user to delete a recipe, we will allow them to delete meal plans that they no longer want. The view logic already exists to make this work so we need to implement the controller action.

app/controllers/meal_plans_controller.rb

def destroy
  @meal_plan = current_user.meal_plans.find(params[:id])
  @meal_plan.destroy
  redirect_to meal_plans_path, notice: "Meal plan deleted!"
end

This basically matches what we’ve done before. Remember how we create a Meal for each date in the MealPlan? We should probably delete those whenever we delete the parent MealPlan. Thankfully, Rails/ActiveRecord makes this easy for use to set up on the association.

app/models/meal_plan.rb

has_many :meals, -> { order(:date) }, inverse_of: :meal_plan, dependent: :destroy

Adding the dependent: :destroy portion ensures that when this object is destroyed that it also destroys the meals.

Editing a Meal Plan

Lastly, we need to allow for a user to edit a meal plan. Here’s the edit view:

app/views/meal_plans/edit.html.erb

<h1>Edit Meal Plan: <%= @meal_plan %></h1>

<%= render(partial: "form") %>

Now onto the controller actions (edit & update). These should be looking familiar by now:

app/controllers/meal_plans_controller.rb

def edit
  @meal_plan = current_user.meal_plans.find(params[:id])
end

def update
  @meal_plan = current_user.meal_plans.find(params[:id])

  if @meal_plan.update_attributes(meal_plan_params)
    redirect_to meal_plan_path(@meal_plan), notice: "Meal plan updated!"
  else
    @errors = @meal_plan.errors.full_messages
    render :edit
  end
end

Recap

That’s the whole thing, we’ve successfully implemented the CRUD interactions for meal plans. Since this isn’t the first time we’ve implemented CRUD we went through this pretty quickly, but there were a few new things shown today:

  • Customizing how an object is rendered to the page via to_s.
  • Accepting nested attributes as part of a form submission.
  • How to potentially clean up views by extracting shared partials. We’ve seen partials before but they were always used by views in the same controller so shared/errors is a little different.

I hope you’ve enjoyed this tutorial, next week we’ll take a detour from adding new functionality to the application to improve the aesthetics and usability of our pages.