Tags: phoenix, elixir, pharchive


Today we are going to upgrade Pharchive to handle associations within our forms. We are also going to add the ability to bootstrap parent records from child forms


Upgrading Pharchive

First off, lets change the default page root for the application by going into the router and changing PageController to CollectionController. Since we are still using the same verb (index), we do not have to change anything else to get our desired result.

file: router.ex

We need a way to assign a Manufacturer to a Film in our form, so we are going to add a plug in our FilmController. This plug is going to place a current list of Manufacturers in the connection for the :new, :create, :edit, and :update actions. From there we will be able to modify our templates to get a select menu in our Film form.

The load_manufacturers function returns nothing, and its sole purpose is to get a current list of Manufacturers from the database, format them into a list that the select form helper can accept, and assign that list to the :manufacturers variable in the connection. Notice that we are only selecting the Manufacturer’s name and its id, so our list will have the structure: [..., {name<string>, id<integer>}, ...].

file: controllers/film_controller.ex

Next we need to modify our templates to give our form access to the :manufacturers connection variable. We do this in our new template and our edit template. Then we make sure that our form template makes use of the new variable by adding a new form-group.

file: templates/film/edit.html.eex

file: templates/film/form.html.eex

file: templates/film/new.html.eex

Currently, we have no way of adding a Manufacturer inline. This would make creating records a bit easier (considering I have a couple hundred rolls of film to catalog, easy is my favorite word!); Let’s change this now.

Now we make sure that our models are related in such a way that we can create them independent of one another. By making our _id fields optional, we can create a model without its relation at the same time. This is very valuable to me because I may just want to create all my Collections first, rather than having to create all my Films/Manufacturers first.

file: models/collection.ex

file: models/film.ex

file: models/manufacturer.ex

Now, we need a way to build from the “bottom up” (e.g. building parent models from child models). Since we have two levels of this to work through, we are going to start with the simpler pair of models Manufacturer -> Film. To restate the problem another way, we need a way to build a film whos manufacturer has not been created yet, and to subsequently build that manufacturer after the film has been created, and finally associate the two records. We are going to do this all on the server, no need to do anything fancy with js at this point.

First, we need a way for the film form to pass a special value for “manufacturer” which signifies that a manufacturer will need to be built next. I have opted to do this by adding an additional option to our form’s manufacturer select value. Since the values for each one of our options correspond to manufacturer ids in the database, I think its fair to use a negative value as a flag (-1). This does not break our parsing logic within the create action (which using a string would).

Now in our create action we have two possible workflows:

  1. The case when a film needs to be associated with an existing manufacturer
  2. The case when a film needs to be created and then redirect to the new manufacturer form.

For case 1, we can leverage Ecto’s build_assoc function, which allows us to bootstrap a Film changeset from a Manufacturer record. For case 2, we need to do a bit more. First we need to created a film changeset which does not include manufacturer_id (our manufacturer_id is -1, this won’t fly). Then we need to add a conditional to our insert logic, and in the case of a new manufacturer, redirect to the new manufacturer path with our new film_id as a query string parameter.

file: controllers/film_controller.ex

Now that we have everything set up on the Film side, it is time to do the same on the Manufacturer’s side.

First we need to provide a place in our manufacturer form to hold a film_id if it’s provided. We do this by adding a hidden_input to our form and referencing the @film_id assigns variable which we set in the ManufacturerController. Since we now have two possible ways to hit new, we can take advantage of Elixir’s pattern matching and desugaring to conditionally set film_id in the connection. Finally, we update our create logic to handle updating a Film if a film is provided.

file: controllers/manufacturer_controller.ex

file: templates/manufacturer/form.html.eex

file: templates/manufacturer/new.html.eex

It would be a nice touch to make manufacturer names show up in the Film index and in Film show; lets do that now. Keep in mind that we need to preload our associated records in order to work with them.

file: controllers/film_controller.ex

file: templates/film/index.html.eex

file: templates/film/show.html.eex

As a first step for creating a bottom up workflow for collections, we need to add a dropdown select to the collection form in the same way that we did with manufacturers for the film form.

file: controllers/collection_controller.ex

file: templates/collection/edit.html.eex

file: templates/collection/form.html.eex

  <div class="form-group">
    <%= label f, :film_id, "Films", class: "control-label" %>
    <%= select f, :film_id, @films, class: "form-control" %>

file: templates/collection/new.html.eex

Next we follow the same pattern as we did with Film. On successful insertion of our Collection, if the ‘New Film’ flag is present, we kick forward a query parameter to the new film form.

file: controllers/collection_controller.ex

Now we repeat the same process we did for Manufacturers on Film, with Collections. The code is effectively copypasta so I will not discuss it in depth.

file: controllers/film_controller.ex

form: templates/film/form.html.eex

file: templates/film/new.html.eex

And that is all for today folks! We have made sure that our associations are working as expected, as well as creating a wizard like process for building parent associations from child records.

Check out the working code here.

In the next post I will be providing some fixtures for our developing application, and we will take on the hydra im sure some of you are wondering about “What about testing?”. Stay tuned loyal viewers, the Phoenix will rise again!