-
-
Save jameslutley/3d6575805f51929dd1a528da89e1a345 to your computer and use it in GitHub Desktop.
Revisions
-
mjrode revised this gist
Jan 25, 2017 . 1 changed file with 16 additions and 13 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -17,7 +17,8 @@ This generator created everything we need except for the proper routes. To fix t Your `web/router.ex` file should look like this. (… indicates truncated code) ```elixir defmodule NestedForms.Router do use NestedForms.Web, :router ... scope "/", NestedForms do @@ -40,7 +41,8 @@ This should look very similar to what we did earlier with users but now we have There is one final step we have to take to finish this relation and that is to add `has_many :posts, NestedForms.Post` to the `User` model. Open up `web/models/user.ex` and change it to look like below. ```elixir defmodule NestedForms.User do use NestedForms.Web, :model schema "users" do @@ -70,7 +72,7 @@ In order to do this we have to make a few changes to our form and controller. First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#inputs_for/4) to build out our nested form. `inputs_for` allows us to attach nested data to the form. (If you are coming from a Rails background this should look similar to `fields_for` and `accepts_nested_attributes_for`.) Add the following to the your form. ```elixir <%= inputs_for f, :posts, fn p -> %> <div class="form-group"> <%= label p, :body, class: "control-label" %> @@ -81,7 +83,7 @@ First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for ``` Now your form should look like this: ```elixir ... <div class="form-group"> @@ -111,7 +113,7 @@ You may be thinking, “But why Mike? I just did all this work updating my form, If we take a look at our `new` action in our `UserController` we can see where the `changeset` is coming from. We need to update this `changeset` so it includes an empty `Post` by default. Your new action should now look like this: ```elixir def new(conn, _params) do changeset = User.changeset(%User{posts: [ %NestedForms.Post{} @@ -141,7 +143,7 @@ There is any easy way to fix this using [cast_assoc](https://hexdocs.pm/ecto/Ect Note: If you want to use `cast_assoc` you need to make sure that the association is already preloaded in the `changeset` struct. We have already taken care of this above so we will be fine. Your `changeset` should now look like this: ```elixir defmodule NestedForms.User do use NestedForms.Web, :model @@ -173,7 +175,7 @@ Thats no biggie, lets take a look at what is happening here. The error explains So lets head back on over to our `UserController` and make the following change to our `edit` action. ```elixir def edit(conn, %{"id" => id}) do user = Repo.get!(User, id) |> Repo.preload(:posts) changeset = User.changeset(user) @@ -185,7 +187,7 @@ By piping our user through to `Repo.preload(:posts)` we are letting Ecto know th Change your update action to look like this. ```elixir def update(conn, %{"id" => id, "user" => user_params}) do user = Repo.get!(User, id) |> Repo.preload(:posts) ... @@ -201,7 +203,7 @@ First lets looks into deleting individual `Posts`. Open up your `Posts` model a Your `Posts` model should now look like this ```elixir defmodule NestedForms.Post do use NestedForms.Web, :model @@ -280,7 +282,7 @@ Now for the tricky part, how do we dynamically add `Posts` to the form? Big than Open up `web/templates/user/_form.html.eex` and add `<%= link_to_post_fields %>` to your form. Your form should now look like this: ```elixir ... <div class="form-group"> <%= label p, :delete, "Delete?", class: "control-label" %> @@ -300,7 +302,8 @@ Your form should now look like this: The `link_to_post_fields` does not yet exist so we need to head on over to `web/views/user_view.ex` and create it. This is what your `UserView` should look like. ```elixir defmodule NestedForms.UserView do use NestedForms.Web, :view alias NestedForms.User alias NestedForms.Post @@ -328,7 +331,7 @@ Finally we need to return the actual link we want to create. We can pass the tem Now that we have our `UserView` , lets create our template. Create a new file called `web/templates/user/post_fields.html.eex` It should look like this: ```elixir <%= inputs_for @f, :posts, fn p -> %> <div id="new-post"> <div class="form-group"> @@ -350,7 +353,7 @@ Now head on over to `app.js` and we are going to add a little javascript to get Add this to your `web/static/js/app.js` ```javascript var el = document.getElementById('add_post'); el.onclick = function(e){ e.preventDefault() -
mjrode revised this gist
Jan 25, 2017 . 1 changed file with 193 additions and 25 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ # The Complete Guide to Nested Forms in Phoenix I recently spent some time dealing with nested forms in Phoenix. Nested forms are great when you want to create multiple database records in a single transaction and associate them with each other. I am new to Phoenix and really struggled to find any resources that helped me with my specific problem. I decided to document what I learned in the process in hopes of helping others that are new to Elixir and Phoenix. Here is my attempt at a one stop shop to learn everything you will need to know about nested forms. If you would like to view the GitHub repo you can check it out [here.](https://github.com/LessEverything/nested_forms) @@ -17,8 +17,7 @@ This generator created everything we need except for the proper routes. To fix t Your `web/router.ex` file should look like this. (… indicates truncated code) ```defmodule NestedForms.Router do use NestedForms.Web, :router ... scope "/", NestedForms do @@ -33,15 +32,15 @@ end For this example we are going to nest a `Post` inside of the `User` form. So we will have a `User` that has many `Posts` . Nested forms are great when So with that in mind lets scaffold out our `Posts`. In your terminal run `mix phoenix.gen.model Post posts body:string user_id:references:users`. This should look very similar to what we did earlier with users but now we have added `user_id:references:users` to the end. This will ensure there is a relation between our models, making it so a `User` has many `Posts` and a `Post` belongs to a `User`. This automatically will add `belongs_to :user, NestedForms.User` in your post model and will create a `user_id`foreign key in your `Post` schema. There is one final step we have to take to finish this relation and that is to add `has_many :posts, NestedForms.Post` to the `User` model. Open up `web/models/user.ex` and change it to look like below. ```defmodule NestedForms.User do use NestedForms.Web, :model schema "users" do @@ -60,9 +59,9 @@ If you are new to Phoenix and are confused by the code that was generated I high Now is a good time to fire up the server and check out what the generators have built for us so far. Run `mix phoenix.server` in your terminal and navigate to `http://localhost:4000/users`. (Wow, doesn’t that look pretty! Phoenix comes preloaded with Bootstrap and so all of the generated code looks pretty nice straight out of the box.) You will see that we can click New User and we are taken to a form that allows us to create a new `User`. The is great, but we want to be able to create a new `User` and a `Post` at the same time from within the same form. [image:5EDCB6C2-D3E6-4C3A-A51B-FEE75E4BA6CB-41694-0001910F151E43B6/Screen Shot 2017-01-23 at 2.53.09 PM.png] @@ -71,7 +70,7 @@ In order to do this we have to make a few changes to our form and controller. First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#inputs_for/4) to build out our nested form. `inputs_for` allows us to attach nested data to the form. (If you are coming from a Rails background this should look similar to `fields_for` and `accepts_nested_attributes_for`.) Add the following to the your form. ``` <%= inputs_for f, :posts, fn p -> %> <div class="form-group"> <%= label p, :body, class: "control-label" %> @@ -82,7 +81,7 @@ First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for ``` Now your form should look like this: ``` ... <div class="form-group"> @@ -112,7 +111,7 @@ You may be thinking, “But why Mike? I just did all this work updating my form, If we take a look at our `new` action in our `UserController` we can see where the `changeset` is coming from. We need to update this `changeset` so it includes an empty `Post` by default. Your new action should now look like this: ``` def new(conn, _params) do changeset = User.changeset(%User{posts: [ %NestedForms.Post{} @@ -121,30 +120,28 @@ Your new action should now look like this: end ``` Now if we head back over to our form we should finally see what we are looking for, [image:FD6861E2-84C9-465A-B7B8-0ECC234B9114-41694-000190F37420561A/Screen Shot 2017-01-23 at 3.16.59 PM.png] Congrats! We are getting closer. Go ahead and try to create a new `User` with a `Post` just to see what happens. If you have everything set up properly you should be redirected to the index page and you should see the `User` you just created. So far so good, right? Well not exactly there are still a number of issues we need to iron out. Lets take a deeper look and see exactly how our `User` and `Post` were saved. I grabbed these screenshots from [Postico](https://eggerapps.at/postico/) . We can see that when we check out the `User` data we attempted to persist everything looks great. [image:67D470B8-0136-4E48-A000-62FA4A0D1C71-41694-000190F374A1A8EE/Screen Shot 2017-01-23 at 3.20.03 PM.png] The problem is when we look for the `Post` data, we can see that it was not saved to the database. [image:66E96E7B-CAA6-40B6-930B-6115B1D57A8D-41694-000190F375048545/Screen Shot 2017-01-23 at 3.20.26 PM.png] This threw me off for a while but I eventually tracked down the issue. The problem is we never informed the `User` `changeset` that it was supposed to cast any nested associations from the params structure. There is any easy way to fix this using [cast_assoc](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3) function in Ecto. Head on over to `web/models/user.ex` and add `|> cast_assoc(:posts, required: true)` to your `changeset`. Note: If you want to use `cast_assoc` you need to make sure that the association is already preloaded in the `changeset` struct. We have already taken care of this above so we will be fine. Your `changeset` should now look like this: ``` defmodule NestedForms.User do use NestedForms.Web, :model @@ -159,15 +156,13 @@ end ``` I found this little blurb from Jose Valim that did a great job explaining what `cast_assoc` is doing: > Note we are using cast_assoc instead of put_assoc in this example. Both functions are defined in Ecto.Changeset. cast_assoc (or cast_embed) is used when you want to manage associations or embeds based on external parameters, such as the data received through Phoenix forms. In such cases, Ecto will compare the data existing in the struct with the data sent through the form and generate the proper operations. On the other hand, we use put_assoc (or put_embed) when we already have the associations (or embeds) as structs and changesets, and we simply want to tell Ecto to take those entries as is. > Now if we take a look back at Postico we can see we are getting the results we were looking for. [image:AEFAAD9E-C8A7-4E59-8EF3-75B6CE7298A3-41694-000190F3754BCBBC/Screen Shot 2017-01-23 at 3.45.41 PM.png] In addition to allowing the post to persist `cast_assoc` also sets the `user_id` to establish the relation between the `User` and `Post`. Ok, now things are really looking great, we are able to create a `User` and a `Post` from the same form. Now go ahead and try to `edit` the `User` you just created. @@ -178,7 +173,7 @@ Thats no biggie, lets take a look at what is happening here. The error explains So lets head back on over to our `UserController` and make the following change to our `edit` action. ``` def edit(conn, %{"id" => id}) do user = Repo.get!(User, id) |> Repo.preload(:posts) changeset = User.changeset(user) @@ -190,14 +185,187 @@ By piping our user through to `Repo.preload(:posts)` we are letting Ecto know th Change your update action to look like this. ``` def update(conn, %{"id" => id, "user" => user_params}) do user = Repo.get!(User, id) |> Repo.preload(:posts) ... end ``` Now save those changes and head back try to edit your `User` again. If you a change and click save you can see that everything is working just like we planned. At this point I was pretty happy with what I had set up but there were still a few things I needed to sort out. First, how could I delete certain `Posts` without deleting the entire `User`. I also needed to figure out how I could dynamically add additional `Posts` from the form. First lets looks into deleting individual `Posts`. Open up your `Posts` model and we are going to add a delete field to our schema. Your `Posts` model should now look like this ``` defmodule NestedForms.Post do use NestedForms.Web, :model schema "posts" do field :body, :string field :delete, :boolean, virtual: true belongs_to :user, NestedForms.User timestamps() end @doc """ Builds a changeset based on the `struct` and `params`. """ def changeset(struct, params \\ %{}) do struct |> cast(params, [:body], [:delete]) |> set_delete_action |> validate_required([:body]) end defp set_delete_action(changeset) do if get_change(changeset, :delete) do %{changeset | action: :delete} else changeset end end end ``` Lets go over the changes we made here to the `Post` model. First we added a `delete` field to our post schema. By passing the option `virtual: true` to the field we are telling Phoenix that we do not want to persist this field to the database. Virtual fields exist temporarily in the schema and are very helpful for local processes and validations. Now we need to pass `:delete` as a required parameter to `cast` The next step is to create the function `set_delete_action`. This function takes a `changeset` as an argument and only runs if the `:delete` key is present in the `changeset`. In order to check if the `:delete` key is present in the `changeset` we are using `get_change`. `get_change` returns the value of the key you passed in if that key is present and `nil` if it is not presesnt. If we have marked a `Post` to be deleted then we will merge in `action: delete` into the `Post` `changeset` which will inform Phoenix to delete that post. Now all we have to do is add the delete field to our form. Go ahead and open up `web/templates/user/_form.html.eex` and make the following changes. ``` ... <%= inputs_for f, :posts, fn p -> %> <div class="form-group"> <%= label p, :body, class: "control-label" %> <%= text_input p, :body, class: "form-control" %> <%= error_tag p, :body %> </div> <div class="form-group"> <%= label p, :delete, "Delete?", class: "control-label" %> <%= checkbox p, :delete %> </div> <% end %> ... <% end %> ``` Now if we refresh the edit page for our `User` we can see that we have the option to delete a `Post`. [image:20146CC0-D81F-41F0-A7C8-BE573F62B010-9355-000010D9BD334DC8/Screen Shot 2017-01-24 at 8.45.25 PM.png] Now for the tricky part, how do we dynamically add `Posts` to the form? Big thanks to [schmitty](http://www.schmitty.me/nested-forms-part-2/) for an excellent video tutorial on this. Open up `web/templates/user/_form.html.eex` and add `<%= link_to_post_fields %>` to your form. Your form should now look like this: ``` ... <div class="form-group"> <%= label p, :delete, "Delete?", class: "control-label" %> <%= checkbox p, :delete %> </div> <% end %> <%= link_to_post_fields %> <div class="form-group"> <%= submit "Submit", class: "btn btn-primary" %> </div> ... ``` The `link_to_post_fields` does not yet exist so we need to head on over to `web/views/user_view.ex` and create it. This is what your `UserView` should look like. ```defmodule NestedForms.UserView do use NestedForms.Web, :view alias NestedForms.User alias NestedForms.Post def link_to_post_fields do changeset = User.changeset(%User{posts: [%Post{}]}) form = Phoenix.HTML.FormData.to_form(changeset, []) fields = render_to_string(__MODULE__, "post_fields.html", f: form) link "Add Post", to: "#", "data-template": fields, id: "add_post" end end ``` Lets walk through the code in our `UserView` First we are going to `alias` `User` and `Post` to make them easier to work with. Our next step is to create an empty `User changeset` with an empty `Post` nested inside. You may notice this is the exact same way we created the `changeset` in our `new` action. Now we need to create our form data. We are going to use the `Phoenix.HTML.FormData` protocol to convert a data structure into a `Phoenix.HTML.Form` struct using the [to_form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.FormData.html#to_form/2) function. Now we are going to use this form to generate our fields that we will use as a template. [render_to_string](https://hexdocs.pm/phoenix/Phoenix.View.html#render_to_string/3) is going to take the module (`__MODULE__` is a macro that returns the current module name as an atom), the template, and allow us to assign our form to `f`. Finally we need to return the actual link we want to create. We can pass the template we just created in as a data attribute to that link. Now that we have our `UserView` , lets create our template. Create a new file called `web/templates/user/post_fields.html.eex` It should look like this: ``` <%= inputs_for @f, :posts, fn p -> %> <div id="new-post"> <div class="form-group"> <%= label p, :body, class: "control-label" %> <%= text_input p, :body, class: "form-control" %> <%= error_tag p, :body %> </div> </div> <% end %> ``` After we have that set up we can go and inspect that link to get a better understanding of what is happening. [image:5FCADC10-F3AF-4AD3-8C3B-08043A1622E9-9355-000024FB56454FB2/Screen Shot 2017-01-25 at 12.59.12 PM.png] You can see that we have the `data-template` with all of the fields automatically generated from the form data. Our next step is to get the link to actually insert this into our DOM. Now head on over to `app.js` and we are going to add a little javascript to get everything working. Add this to your `web/static/js/app.js` ``` var el = document.getElementById('add_post'); el.onclick = function(e){ e.preventDefault() var el = document.getElementById('add_post'); let time = new Date().getTime() let template = el.getAttribute('data-template') var uniq_template = template.replace(/\[0]/g, `[${time}]`) uniq_template = uniq_template.replace(/\[0]/g, `_${time}_`) this.insertAdjacentHTML('afterend', uniq_template) }; ``` Now with that little bit of javascript we should be all set! You can now add as many posts as you would like for each `User`. [image:938A414D-6FCD-48A9-965B-AC456BA4535C-9355-00002A60A3B683BD/Screen Shot 2017-01-25 at 2.56.47 PM.png] I hope you found this post helpful, if you have any questions please leave a comment below. -
mjrode revised this gist
Jan 24, 2017 . 1 changed file with 14 additions and 11 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -33,14 +33,13 @@ end For this example we are going to nest a `Post` inside of the `User` form. So we will have a `User` that has many `Posts` . So with that in mind lets scaffold out our `Posts`. In your terminal run `mix phoenix.gen.model Post posts body:string user_id:references:users`. This should look very similar to what we did earlier with users but now we have added `user_id:references:users` to the end. This automatically will add `belongs_to :user, NestedForms.User` in your post model and will create a `user_id`foreign key in your `Post` schema. There is one final step we have to take to finish this relation and that is to add `has_many :posts, NestedForms.Post` to the `User` model. Open up `web/models/user.ex` and change it to look like below. ```elixir defmodule NestedForms.User do use NestedForms.Web, :model @@ -61,9 +60,9 @@ If you are new to Phoenix and are confused by the code that was generated I high Now is a good time to fire up the server and check out what the generators have built for us so far. Run `mix phoenix.server` in your terminal and navigate to `http://localhost:4000/users`. (Wow, doesn’t that look pretty! Phoenix comes preloaded with Bootstrap and so all of the generated code looks decent straight out of the box.) You will see that we can click New User and we are taken to a form that allows us to create a new `User`. The is nice but we want to be able to create a new `User` and a `Post` at the same time from within the same form. [image:5EDCB6C2-D3E6-4C3A-A51B-FEE75E4BA6CB-41694-0001910F151E43B6/Screen Shot 2017-01-23 at 2.53.09 PM.png] @@ -122,23 +121,25 @@ Your new action should now look like this: end ``` Now if we head back over to our form we should finally see what we are looking for. [image:FD6861E2-84C9-465A-B7B8-0ECC234B9114-41694-000190F37420561A/Screen Shot 2017-01-23 at 3.16.59 PM.png] Congrats! We are getting closer. Go ahead and try to create a new `User` with a `Post` just to see what happens. If you have everything set up properly you should be redirected to the index page and you should see the `User` you just created. So far so good, right? Well, not exactly, there are still a number of issues we need to iron out. Lets take a deeper look and see exactly how our `User` and `Post` were saved. I grabbed these screenshots from [Postico](https://eggerapps.at/postico/) . We can see that when we check out the `User` data we attempted to persist everything looks great. [image:67D470B8-0136-4E48-A000-62FA4A0D1C71-41694-000190F374A1A8EE/Screen Shot 2017-01-23 at 3.20.03 PM.png] The problem is when we look for the `Post` data, we can see that it was not saved to the database. [image:66E96E7B-CAA6-40B6-930B-6115B1D57A8D-41694-000190F375048545/Screen Shot 2017-01-23 at 3.20.26 PM.png] This threw me off for a while but I eventually tracked down the issue. The problem is we never informed the `User` `changeset` that it was supposed to cast any nested associations from the params structure. There is any easy way to fix this using the [cast_assoc](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3) function in Ecto. Head on over to `web/models/user.ex` and add `|> cast_assoc(:posts, required: true)` to your `changeset`. Note: If you want to use `cast_assoc` you need to make sure that the association is already preloaded in the `changeset` struct. We have already taken care of this above so we will be fine. @@ -158,13 +159,15 @@ end ``` I found this little blurb from Jose Valim that did a great job explaining what `cast_assoc` is doing: > Note we are using cast_assoc instead of put_assoc in this example. Both functions are defined in Ecto.Changeset. cast_assoc (or cast_embed) is used when you want to manage associations or embeds based on external parameters, such as the data received through Phoenix forms. In such cases, Ecto will compare the data existing in the struct with the data sent through the form and generate the proper operations. On the other hand, we use put_assoc (or put_embed) when we already have the associations (or embeds) as structs and changesets, and we simply want to tell Ecto to take those entries as is. > Now if we take a look back at Postico we can see we are getting the results we were looking for. [image:AEFAAD9E-C8A7-4E59-8EF3-75B6CE7298A3-41694-000190F3754BCBBC/Screen Shot 2017-01-23 at 3.45.41 PM.png] In addition to allowing the `Post` to persist `cast_assoc` also sets the `user_id` to establish the relation between the `User` and `Post`. Ok, now things are really looking great, we are able to create a `User` and a `Post` from the same form. Now go ahead and try to `edit` the `User` you just created. @@ -194,7 +197,7 @@ def update(conn, %{"id" => id, "user" => user_params}) do end ``` Now save those changes and head back and try to edit your `User` again. If you make a change and click save you can see that everything is working just like we planned. -
mjrode revised this gist
Jan 24, 2017 . 1 changed file with 10 additions and 8 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -17,7 +17,8 @@ This generator created everything we need except for the proper routes. To fix t Your `web/router.ex` file should look like this. (… indicates truncated code) ```elixir defmodule NestedForms.Router do use NestedForms.Web, :router ... scope "/", NestedForms do @@ -40,7 +41,8 @@ This should look very similar to what we did earlier with users but now we have There is one final step we have to take to finish this relation and that is to add `has_many :posts, NestedForms.Post` to the `User` model. Open up `web/models/user.ex` and change it to look like below. ```elixir defmodule NestedForms.User do use NestedForms.Web, :model schema "users" do @@ -70,7 +72,7 @@ In order to do this we have to make a few changes to our form and controller. First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#inputs_for/4) to build out our nested form. `inputs_for` allows us to attach nested data to the form. (If you are coming from a Rails background this should look similar to `fields_for` and `accepts_nested_attributes_for`.) Add the following to the your form. ```elixir <%= inputs_for f, :posts, fn p -> %> <div class="form-group"> <%= label p, :body, class: "control-label" %> @@ -81,7 +83,7 @@ First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for ``` Now your form should look like this: ```elixir ... <div class="form-group"> @@ -111,7 +113,7 @@ You may be thinking, “But why Mike? I just did all this work updating my form, If we take a look at our `new` action in our `UserController` we can see where the `changeset` is coming from. We need to update this `changeset` so it includes an empty `Post` by default. Your new action should now look like this: ```elixir def new(conn, _params) do changeset = User.changeset(%User{posts: [ %NestedForms.Post{} @@ -141,7 +143,7 @@ There is any easy way to fix this using [cast_assoc](https://hexdocs.pm/ecto/Ect Note: If you want to use `cast_assoc` you need to make sure that the association is already preloaded in the `changeset` struct. We have already taken care of this above so we will be fine. Your `changeset` should now look like this: ```elixir defmodule NestedForms.User do use NestedForms.Web, :model @@ -173,7 +175,7 @@ Thats no biggie, lets take a look at what is happening here. The error explains So lets head back on over to our `UserController` and make the following change to our `edit` action. ```elixir def edit(conn, %{"id" => id}) do user = Repo.get!(User, id) |> Repo.preload(:posts) changeset = User.changeset(user) @@ -185,7 +187,7 @@ By piping our user through to `Repo.preload(:posts)` we are letting Ecto know th Change your update action to look like this. ```elixir def update(conn, %{"id" => id, "user" => user_params}) do user = Repo.get!(User, id) |> Repo.preload(:posts) ... -
mjrode revised this gist
Jan 24, 2017 . 1 changed file with 60 additions and 30 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,17 +1,23 @@ # Nested Forms in Phoenix I recently spent some time dealing with nested forms in Phoenix. Nested forms are great when you want to create multiple database records in a single transaction and associate them with each other. I am new to Phoenix and really struggled to find any resources that helped me with my specific problem. I decided to document what I learned in the process in hopes of helping others that are new to Elixir and Phoenix. Here is my attempt at a one stop shop to learn everything you will need to know about nested forms. If you would like to view the GitHub repo you can check it out [here.](https://github.com/LessEverything/nested_forms) Thanks to [Heartbeat ](http://blog.heartbeathq.com/storing-nested-associations-with-phoenix-forms/) and [Jose ](http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/) for excellent blog posts on nested forms. Also shoutout to [Josh](https://twitter.com/GoHard_EveryDay) for showing me some examples at RubyJax. I want to make this guide as thorough as possible so the only assumption I am going to make is that you already have Phoenix and Elixir installed. If this is not the case pause now and follow [this](http://www.phoenixframework.org/docs/installation) getting started guide. With that taken care of lets go ahead and create a new application called `nested_forms`. To do this we are going to run `mix phoenix.new nested_forms` in our terminal and then `cd nested_forms`. We can now start to scaffold out a basic application to work with. In your terminal run ` mix phoenix.gen.html User users name:string`. This is going to generate a model and the corresponding view, templates, and controller. `User` is the module name that we are creating, whereas `users` is going to be used as the name for our resources and schema. This generator created everything we need except for the proper routes. To fix this we need to open `web/router.ex` and add `resources "/users", UserController` . Your `web/router.ex` file should look like this. (… indicates truncated code) ```defmodule NestedForms.Router do use NestedForms.Web, :router ... scope "/", NestedForms do @@ -24,11 +30,15 @@ end ``` For this example we are going to nest a `Post` inside of the `User` form. So we will have a `User` that has many `Posts` . Nested forms are great when So with that in mind lets scaffold out our `Posts`. In your terminal run `mix phoenix.gen.model Post posts body:string user_id:references:users`. This should look very similar to what we did earlier with users but now we have added `user_id:references:users` to the end. This will ensure there is a relation between our models, making it so a `User` has many `Posts` and a `Post` belongs to a `User`. This automatically will add `belongs_to :user, NestedForms.User` in your post model and will create a `user_id`foreign key in your `Post` schema. There is one final step we have to take to finish this relation and that is to add `has_many :posts, NestedForms.Post` to the `User` model. Open up `web/models/user.ex` and change it to look like below. ```defmodule NestedForms.User do use NestedForms.Web, :model @@ -48,13 +58,18 @@ Now that we have our models setup lets get our database up and running. First we If you are new to Phoenix and are confused by the code that was generated I highly recommend getting [Programming Phoenix](https://pragprog.com/book/phoenix/programming-phoenix) by Chris McCord. He does a great job walking you through how to build a simple Phoenix application without using generators. Now is a good time to fire up the server and check out what the generators have built for us so far. Run `mix phoenix.server` in your terminal and navigate to `http://localhost:4000/users`. (Wow, doesn’t that look pretty! Phoenix comes preloaded with Bootstrap and so all of the generated code looks pretty nice straight out of the box.) You will see that we can click New User and we are taken to a form that allows us to create a new `User`. The is great, but we want to be able to create a new `User` and a `Post` at the same time from within the same form. [image:5EDCB6C2-D3E6-4C3A-A51B-FEE75E4BA6CB-41694-0001910F151E43B6/Screen Shot 2017-01-23 at 2.53.09 PM.png] In order to do this we have to make a few changes to our form and controller. First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#inputs_for/4) to build out our nested form. `inputs_for` allows us to attach nested data to the form. (If you are coming from a Rails background this should look similar to `fields_for` and `accepts_nested_attributes_for`.) Add the following to the your form. ``` <%= inputs_for f, :posts, fn p -> %> <div class="form-group"> @@ -65,7 +80,7 @@ First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for <% end %> ``` Now your form should look like this: ``` ... @@ -89,9 +104,13 @@ Now your form should look like below: <% end %> ``` Let’s head back to `http://localhost:4000/users` and try to create a new `User`. We can see that even though we updated our form we still do not get an option to create a post. You may be thinking, “But why Mike? I just did all this work updating my form, shouldn’t it show up”. Well I had similar thoughts when I was trying to get this work. If you take a closer look at the form you can see that it accepts a `changeset` as an argument. So lets take a look at that `changeset` we are passing in and see what adjustments we need to make. If we take a look at our `new` action in our `UserController` we can see where the `changeset` is coming from. We need to update this `changeset` so it includes an empty `Post` by default. Your new action should now look like this: ``` def new(conn, _params) do changeset = User.changeset(%User{posts: [ @@ -101,21 +120,27 @@ Lets head over to our `UserController` and make a few changes to get our form wo end ``` Now if we head back over to our form we should finally see what we are looking for, [image:FD6861E2-84C9-465A-B7B8-0ECC234B9114-41694-000190F37420561A/Screen Shot 2017-01-23 at 3.16.59 PM.png] Congrats! We are getting closer. Go ahead and try to create a new `User` with a `Post` just to see what happens. If you have everything set up properly you should be redirected to the index page and you should see the `User` you just created. So far so good, right? Well not exactly there are still a number of issues we need to iron out. Lets take a deeper look and see exactly how our `User` and `Post` were saved. I grabbed these screenshots from [Postico](https://eggerapps.at/postico/) . We can see that when we check out the `User` data we attempted to persist everything looks great. [image:67D470B8-0136-4E48-A000-62FA4A0D1C71-41694-000190F374A1A8EE/Screen Shot 2017-01-23 at 3.20.03 PM.png] The problem is when we look for the `Post` data, we can see that it was not saved to the database. [image:66E96E7B-CAA6-40B6-930B-6115B1D57A8D-41694-000190F375048545/Screen Shot 2017-01-23 at 3.20.26 PM.png] This threw me off for a while but I eventually tracked down the issue. The problem is we never informed the `User` `changeset` that it was supposed to cast any nested associations from the params structure. There is any easy way to fix this using [cast_assoc](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3) function in Ecto. Head on over to `web/models/user.ex` and add `|> cast_assoc(:posts, required: true)` to your `changeset`. Note: If you want to use `cast_assoc` you need to make sure that the association is already preloaded in the `changeset` struct. We have already taken care of this above so we will be fine. Your `changeset` should now look like this: ``` defmodule NestedForms.User do use NestedForms.Web, :model @@ -132,18 +157,19 @@ end I found this little blurb from Jose Valim that did a great job explaining what `cast_assoc` is doing: > Note we are using cast_assoc instead of put_assoc in this example. Both functions are defined in Ecto.Changeset. cast_assoc (or cast_embed) is used when you want to manage associations or embeds based on external parameters, such as the data received through Phoenix forms. In such cases, Ecto will compare the data existing in the struct with the data sent through the form and generate the proper operations. On the other hand, we use put_assoc (or put_embed) when we already have the associations (or embeds) as structs and changesets, and we simply want to tell Ecto to take those entries as is. > Now if we take a look back at Postico we can see we are getting the results we were looking for. [image:AEFAAD9E-C8A7-4E59-8EF3-75B6CE7298A3-41694-000190F3754BCBBC/Screen Shot 2017-01-23 at 3.45.41 PM.png] In addition to allowing the post to persist `cast_assoc` also sets the `user_id` to establish the relation between the `User` and `Post`. Ok, now things are really looking great, we are able to create a `User` and a `Post` from the same form. Now go ahead and try to `edit` the `User` you just created. You can see we are welcomed with this nice error message. [image:547F0493-C282-4A36-BF28-0EF79E3E303C-41694-000190F375E463BA/Screen Shot 2017-01-23 at 3.16.11 PM.png] Thats no biggie, lets take a look at what is happening here. The error explains to us the we need to `preload` our association. Ecto does not automatically preload associations for you so you need to explicitly tell it when you want this to happen. [Here](https://tkowal.wordpress.com/2016/04/23/nested-preload-in-ecto/) is a great blog post that goes over nested preloads. So lets head back on over to our `UserController` and make the following change to our `edit` action. @@ -155,7 +181,9 @@ So lets head back on over to our `UserController` and make the following change end ``` By piping our user through to `Repo.preload(:posts)` we are letting Ecto know that we would like to preload the associated `Posts`. We also need to make a similar adjustment to our update action. Change your update action to look like this. ``` def update(conn, %{"id" => id, "user" => user_params}) do @@ -164,5 +192,7 @@ def update(conn, %{"id" => id, "user" => user_params}) do end ``` Now save those changes and head back try to edit your `User` again. If you a change and click save you can see that everything is working just like we planned. -
mjrode revised this gist
Jan 23, 2017 . 1 changed file with 2 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -10,7 +10,8 @@ This generator created everything we need except for the proper routes. To fix t Your `web/router.ex` file should look like this. (/… indicates truncated code/) ``` defmodule NestedForms.Router do use NestedForms.Web, :router ... scope "/", NestedForms do -
mjrode created this gist
Jan 23, 2017 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,167 @@ # Nested Forms in Phoenix I recently spent some time dealing with nested forms in Phoenix and found that I needed to take bits and pieces from multiple resources to get everything working. I decided to document what I learned in this process in hopes of helping others that are new to Elixir and Phoenix. Here is my attempt at a one stop shop to learn everything you will need to know about nested forms. I am starting with the assumption that you already have Phoenix and Elixir installed. If this is not the case pause now and follow [this](http://www.phoenixframework.org/docs/installation) getting started guide. With that taken care of we are going to create a new application called `nested_forms`. To do this we are going to run `mix phoenix.new nested_forms` in our terminal and then `cd nested_forms`. We can now start to scaffold out a basic application to work with. In your terminal type in ` mix phoenix.gen.html User users name:string`. This is going to generate a model and the corresponding view, templates, and controller. `User` is the module name that we are creating, whereas `users` is going to be used as the name for our resources and schema. This generator created everything we need except for the proper routes. To fix this we need to open `web/router.ex` and add `resources "/users", UserController` . Your `web/router.ex` file should look like this. (/… indicates truncated code/) ```defmodule NestedForms.Router do use NestedForms.Web, :router ... scope "/", NestedForms do pipe_through :browser # Use the default browser stack get "/", PageController, :index resources "/users", UserController end ... end ``` For this example we are going to nest a `Post` inside of the `User` form. So we will have a user that has many posts and we will have a post model that belongs to a user. So with that in mind lets scaffold out our Posts. In your terminal run `mix phoenix.gen.model Post posts body:string user_id:references:users`. This should look very similar to what we did earlier with users but now we have added `user_id:references:users` to the end. This will ensure there is a relation between our models making it so a user has many posts and a post belongs to a user. There is one final step we have to take to finish this relation and that is to add `has_many :posts, NestedForms.Post` to the user model. Open up `web/models/user.ex` and change it to look like below. ```defmodule NestedForms.User do use NestedForms.Web, :model schema "users" do field :name, :string has_many :posts, NestedForms.Post timestamps() end ... end ``` Now that we have our models setup lets get our database up and running. First we will create the database with `mix ecto.create` then we will run the migrations with `mix ecto.migrate`. If you are new to Phoenix and are confused by the code that was generated I highly recommend getting [Programming Phoenix](https://pragprog.com/book/phoenix/programming-phoenix) by Chris McCord. He does a great job walking you through how to build a simple Phoenix application without using generators. Now is a good time to fire up the server and check out what the generators have built for us so far. Run `mix phoenix.server` in your terminal and navigate to `http://localhost:4000/users`. [image:682558F8-40C3-49CC-8194-36FEC03FE4B0-41694-00015DF7BE4E6A5C/Screen Shot 2017-01-23 at 2.53.09 PM.png] You will see that we can click New User and we are taken to a form that allows us to create a new user. The is great, but we want to be able to create a new user and a post at the same time from within the same form. In order to do this we have to make a few changes to our form and controller. First go to `web/templates/user/_form.html.eex`. We are going to use [inputs_for](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#inputs_for/4) to build out our nested form. Add the following to the your form. ``` <%= inputs_for f, :posts, fn p -> %> <div class="form-group"> <%= label p, :body, class: "control-label" %> <%= text_input p, :body, class: "form-control" %> <%= error_tag p, :body %> </div> <% end %> ``` Now your form should look like below: ``` ... <div class="form-group"> <%= label f, :name, class: "control-label" %> <%= text_input f, :name, class: "form-control" %> <%= error_tag f, :name %> </div> <%= inputs_for f, :posts, fn p -> %> <div class="form-group"> <%= label p, :body, class: "control-label" %> <%= text_input p, :body, class: "form-control" %> <%= error_tag p, :body %> </div> <% end %> <div class="form-group"> <%= submit "Submit", class: "btn btn-primary" %> </div> <% end %> ``` Now if we head back to `http://localhost:4000/users` and try to create a new user we will see that we still do not get an option to create a post. Lets head over to our `UserController` and make a few changes to get our form working. First we need to make a few changes to our new action so it includes an empty post by default. Your new action should now look like below: ``` def new(conn, _params) do changeset = User.changeset(%User{posts: [ %NestedForms.Post{} ]}) render(conn, "new.html", changeset: changeset) end ``` Now if we head back over to our form and refresh we should see something like this. [image:E21D5399-A5B9-406D-9023-B48232A3C77E-41694-00015F349ADD85B7/Screen Shot 2017-01-23 at 3.16.59 PM.png] Congrats! We are getting closer. Go ahead and try to create a new user with a post just to see what happens. If you have everything set up properly you should be redirected to the index page and you should see the user you just created. So far so good, right? Well not exactly there are still a number of issues we need to iron out. Lets take a deeper look and see exactly how our `User` and `Post` were saved. I grabbed these screenshots from Postico. We can see that when we check out the User data we attempted to persist everything looks great. [image:9AD3C6D2-A950-4405-B53F-2F247FB05349-41694-00015F715B9451A3/Screen Shot 2017-01-23 at 3.20.03 PM.png] The problem is when we attempt to look for the `Post` data, we can see that it was not saved to the database. [image:15B1A86E-123B-4303-896A-C8CD9226077C-41694-00015F7A5AC28450/Screen Shot 2017-01-23 at 3.20.26 PM.png] This threw me off for a while but I eventually tracked down the issue. The problem is we never informed the `User` changeset that it was supposed to be accepting the `Post` params. There is any easy way to fix this. Head on over to `web/models/user.ex` and add `|> cast_assoc(:posts, required: true)` to your `changeset` Your changeset should now look like the following: ``` defmodule NestedForms.User do use NestedForms.Web, :model ... def changeset(struct, params \\ %{}) do struct |> cast(params, [:name]) |> cast_assoc(:posts, required: true) |> validate_required([:name]) end end ``` I found this little blurb from Jose Valim that did a great job explaining what `cast_assoc` is doing: > Note we are using cast_assoc instead of put_assoc in this example. Both functions are defined in Ecto.Changeset. cast_assoc (or cast_embed) is used when you want to manage associations or embeds based on external parameters, such as the data received through Phoenix forms. In such cases, Ecto will compare the data existing in the struct with the data sent through the form and generate the proper operations. On the other hand, we use put_assoc (or put_embed) when we already have the associations (or embeds) as structs and changesets, and we simply want to tell Ecto to take those entries as is. Now if we take a look back at Postico we can see we are getting the results we were looking for. [image:E6A3A866-956E-4DF9-8BE4-1902DAB7395D-41694-000160C54F96D07C/Screen Shot 2017-01-23 at 3.45.41 PM.png] In addition to allowing the post to persist `cast_assoc` also sets the `user_id` to establish the relation between the `User` and `Post`. Ok, now things are really looking great, we are able to create a `User` and a `Post` from the same form. Now go ahead and try to `edit` the `User` you just created. You can see we are welcomed with this nice error message. [image:5C5D3538-E0E7-4179-B89F-6B1F5B31D289-41694-000160EFDB0D2266/Screen Shot 2017-01-23 at 3.16.11 PM.png] Thats no biggie, lets take a look at what is happening here. The error explains to us the we need to `preload` our association. Ecto does not automatically preload associations for you so you need to explicitly tell it when you want this to happen. So lets head back on over to our `UserController` and make the following change to our `edit` action. ``` def edit(conn, %{"id" => id}) do user = Repo.get!(User, id) |> Repo.preload(:posts) changeset = User.changeset(user) render(conn, "edit.html", user: user, changeset: changeset) end ``` By piping our user through to `Repo.preload(:posts)` we are letting Ecto know that we would like to preload the associated posts. We also need to make a similar adjustment to our update action. Change your update action to look like below. ``` def update(conn, %{"id" => id, "user" => user_params}) do user = Repo.get!(User, id) |> Repo.preload(:posts) ... end ``` Now save those changes and head back try to edit your `User` again. If you make a few changes and click save you can see that everything is working just like we planned.