Phoenix for Django Devs: Inline FormSet Via Nested Changesets Part 1
by Justin Michalicek on Sept. 17, 2016, 11:18 p.m. UTCA pretty common use case is to create or edit a main object while also manipulating a list of related objects. On the database side the list of objects usually have a foreign key back to your main object's table. Using Django the form, data validation, and saving is handled with a ModelForm while related object saving is handled via an inlineformset_factory. Previous version of Phoenix did not have a very elegant solution to solving the same problem. Posts on Stack Overflow and elsewhere detailed manually handling transactions to create the parent object, then validate the related objects, then save them requiring several steps and a bunch of boilerplate which we use frameworks like Django and Phoenix to avoid. Fortunately, Phoenix 1.2 and Ecto 2.0 provide an excellent solution which is nearly on par with Django as far as complexity and tedious boilerplate.
Django Implementation
First the Django code we would start with the following. A couple of models with a foreign key from Child to Parent, a django forms.ModelForm, a template, and a view.
models.py:
from django.db.models import model class Parent(models.Model): name = models.CharField(max_length=100) description = models.CharField(max_length=100, blank=True, default='') def __unicode__(self): return self.name class Child(models.Model): name = models.CharField(max_length=100) description = models.CharField(max_length=100, blank=True, default='') parent = models.ForeignKey('my_app.Parent') def __unicode__(self): return u'%s child of %s' % (self.name, self.parent.name)
The following code in forms.py defines two ModelForms, one for the Parent and Child, and uses Django's inlineformset_factory to create a FormSet. Inline formsets handle displaying, validating, and saving form for a list of objects which are all owned by the same parent object. This includes automatically adding one or more (or zero if desired) extra, empty forms for adding more items easily.
forms.py:
from __future__ import absolute_import from django import forms from .models import Parent, Child class ParentForm(forms.ModelForm): class Meta: model = Parent fields = ('name', 'description') class ChildForm(forms.ModelForm): class Meta: model = Child fields = ('name', 'description', 'parent') ChildrenFormSet = forms.inlineformset_factory(Parent, Child, form=ChildForm, extra=1)
In the view we instantiate both the ModelForm for Parent and the ChildrenFormSet to add, remove, and modify children of a Parent instance. Since both the parent and the children are being created in this view, the parent instance gets assigned to the ChildrenFormSet after it has been saved. When working with a Parent which already exists, you can specify the instance when instantiating the ChildrenFormSet. In a real world application you would probably wrap this in an atomic block so that the parent and children are created and possibly rolled back in one transaction. I also usually use Django's Class Based Views, but they actually take quite a bit more extra boilerplate for having more than one form instance, so I have not used one for this example.
views.py:
from __future__ import absolute_import from django.shortcuts import render from .forms import ParentForm, ChildrenFormSet from .models import Parent, Child # I normally use class based views, but sticking with function based here because there's less code involved with # handling a simple two form view. It is also a more direct comparison to what will be done with Phoenix. def create_parent(request, *args, **kwargs): if request.method == 'POST': parent_form = ParentForm(request.POST, request.FILES, prefix='parent') children_forms = ChildrenFormSet(request.POST, request.FILES, prefix='children') # validate the forms separately first so that we can show errors for all forms if multiple # forms have an error parent_valid = parent_form.is_valid() children_valid = childrent_forms.is_valid() if parent_valid and children_valid: # save the parent then set it as the instance on the formset before # saving the chldren via the formset so that Django # can create the relationship in the db parent = parent_form.save() children_forms.instance = parent children_forms.save() return HttpResponseRedirect('detail_view', args=[parent.pk]) else: parent_form = ParentForm(prefix='parent') children_forms = ChildrenFormSet(prefix='children') return render(request, 'my_form.html', {'parent_form': parent_form, 'children_forms': children_forms})
my_form.html:
<html> <body> <form method="post" action=""> <!-- Simplest display of django forms. for any complex layout you'll iterate over the fields and render them manually --> {{ parent_form }} {{ children_forms }} <button>Submit</button> </form> </body> </html>
Phoenix Implementation
Phoenix 1.2 and Ecto 2.0 offers a solution which is fairly similar. I believe it is actually less code than what is above and has some thing I like more and other things I like less about how you have to implement it. What I really like is that Phoenix lets you do it all with just one changeset rather than Django's two forms. There is also less extra code involved for generating the forms due to how changesets interact with Ecto schemas which at times is nice and at other times I feel results in cluttering up the model and schema code.
First we need to define the two models, Parent and Child. Parent will make use of has_many/3 on the schema to access the Child instances which have a foreign key to a specific Parent. Child will use belongs_to/3 on the schema to provide access to the related Parent it has a foreign key to.
There are a couple of things to take note of in the code below. The first is that children is not passed through to Parent.cast/3, validating the relationship is handled by cast_assoc/2. The other is the usage of cast_assoc/2 using the with: &MyApp.Child.nested_changeset/2 argument. By default cast_assoc/1 would have used MyApp.Child.changeset/2, but that tries to set the assoc back to the Parent which may not exist yet. Instead we give it a different changeset function to use which does not look at the association back to Parent and rely on cast_assoc/2 to deal with that. In child.ex below the extra nested_changeset/2 is defined.
models/parent.ex:
defmodule MyApp.Parent do use MyApp.Web, :model schema "parent" do field :name, :string field :description, :string has_many :children, MyApp.Child timestamps end @error_messages %{ :name => %{ :required => "Please include a name." }, :description => %{ :required => "Please include a short description." } } def error_messages, do: @error_messages def changeset(model, params \\ %{}) do model |> cast(params, [:name, :description]) |> validate_required(:name, message: @error_messages.name.required) |> validate_required(:description, message: @error_messages.description.required) |> cast_assoc(:location_choices, with: &MyApp.Child.nested_changeset/2) end end
The Child model is below. It defines the usual fields plus an extra virtual field called delete. This virtual field does not exist on the database anywhere, but must be defined on the Ecto Schema to be used. There is then an extra function on the module, mark_for_deletion/1. This function takes a changeset, checks the delete virtual field, and if it is true then the changeset's action is set to :delete. When the changeset is saved, since the action is :delete, the associated Child instance will be removed from the database.
models/child.ex:
defmodule MyApp.Child do use MyApp.Web, :model schema "child" do field :name, :string field :description, :string belongs_to :event, MyApp.Parent # virtual fields for form manipulation field :delete, :boolean, virtual: true<code> timestamps end @doc """ Creates a changeset based on the `model` and `params`. """ def changeset(model, params \\ %{}) do model |> cast(params, [:name, :description]) |> cast_assoc(:parent, required: True) |> validate_required([:name, ]) end @doc """ Returns a changeset which is used when nested in a Parent based changeset. It does not do anything with the parent association since that is handled by Parent's cast_assoc() """ def nested_changeset(model, params \\ %{}) do # Does not try to cast the parent assoc model |> cast(params, [:name, :description]) |> validate_required([:name, ]) |> mark_for_deletion() end @doc """ Checks if the `delete` virtual field was set and if so changes the action on the changeset to :delete so that the item will be removed from the database. """ defp mark_for_deletion(changeset) do if get_change(changeset, :delete) do %{changeset | action: :delete} else changeset end end end
The template can then be set up as follows, using form_for/4 to process and render the main Parent form fields and inputs_for/4 to render the nested Child fields. I am just using the form.html.eex as is the default setup with Phoenix here, so everything else is in a parent template.
templates/parent/form.html.eex:
<%= form_for @changeset, @action, fn f -> %> <%= if @changeset.action do %> <div class="alert alert-danger"> <p>Oops, something went wrong! Please check the errors below.</p> </div> <% end %> <div class="form-group <%= if f.errors[:name], do: "has-danger" %>"> <%= label f, :name, class: "control-label" %> <%= text_input f, :name, class: "form-control" %> <%= error_tag f.source, :name %> </div> <div class="form-group <%= if f.errors[:description], do: "has-danger" %>"> <%= label f, :description, class: "control-label" %> <%= text_input f, :description, class: "form-control" %> <%= error_tag f.source, :descriptin %> </div> <!-- here is our nested changeset. It will iterate over all children of the Parent and show the fields for them --> <!-- The `append` parameter specifies a changeset with a single empty child which is displayed ONLY if parent has no children --> <%= inputs_for f, :children, [append: [Child.nested_changeset(%Child{})]], fn fp -> %> <div class="form-group <%= if f.errors[:name], do: "has-danger" %>"> <%= label f, :name, class: "control-label" %> <%= text_input f, :name, class: "form-control" %> <%= error_tag f.source, :name %> </div> <div class="form-group <%= if f.errors[:description], do: "has-danger" %>"> <%= label f, :description, class: "control-label" %> <%= text_input f, :description, class: "form-control" %> <%= error_tag f.source, :description %> </div> <div class="form-group"> <%= checkbox fp, :delete, class: "hidden-xs-up delete-checkbox" %> </div> <% end %> <!-- end the nested children --> <%= submit "Submit", class: "btn btn-primary pull-xs-right" %> <% end %>
Wrap Up
So there we have how to have a parent and nested set of children in a form with with Django and Phoenix.
What I really like about Django's take on this is the separation of forms from models. The ModelForm class can use the Model as a reference, but you can add form specific fields to the form rather than having them clutter up your model. In the case of the delete checkbox for a FormSet, that is part of the FormSet implementation and is automatically added (with the option to not have it there) without even being on your form. With Phoenix, fields which are only used for a single form and are not really part of the model still live on your model, cluttering things up and causing potential confusion. Django also makes it easier to have an extra form for adding new items to the list all the time while Phoenix' solution only automates adding a form for adding a new child when there are no children and has a way to automatically render all form fields which, while rarely something you'll use in production, lets you get the form displayed for early dev with slightly less hassle.
Phoenix, on the other hand, handles the actual nested relationships in the form more cleanly. It requires less extra code in general to handle the nested models, this is especially true if you go to class based views with Django, which only handle a single form by default and so require overriding several methods. It also removes some added hassle of Django's formsets, which add in an extra management form with some extra metadata. Django formsets need form id's to be sequential which can become troublesome when trying to do more dynamic forms, which most people will need in the end. What I dislike is that it's not clear how to cleanly automatically show a form for adding a new child if your parent already has children, aside from passing in another changeset and wrangling its processing separately. I also dislike the virtual fields on the model instead of having form specific fields separated out into their own module somewhere, as I mentioned above.
Overall, both work well and I am happy to work with either at this point. In the examples above you can only add at most one new item per form submission, although in both cases you could specify multiple extra forms - but you would be limited to however many you display. Next I will extend these to use javascript to have the forms to add a new item to the Child list which is hidden and add or remove as many as you want in one shot with both Django and Phoenix.