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:
- A form to select the date range. Submitting this will re-render the new meal plan page with the proper number of meals.
- 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.