Dynamically add/remove nested fields to your Phoenix forms from the client with a thin JavaScript layer.
- The package can be installed by adding
dynamic_inputs_for
to your list of dependencies inmix.exs
:
def deps do
[{:dynamic_inputs_for, "~> 1.1.0"}]
end
- Then add
dynamic_inputs_for
to your list of dependencies inpackage.json
and runnpm install
. For the default Phoenix structure, inassets/package.json
:
"dependencies": {
"dynamic_inputs_for": "file:../deps/dynamic_inputs_for"
}
- Finally, don't forget to import the module. For the default Phoenix structure, in
assets/js/app.js
:
import "dynamic_inputs_for";
Imagine the following Ecto schemas:
defmodule Shop do
use Ecto.Schema
schema "shops" do
field :name, :string
has_many :products, Product
end
end
defmodule Product do
use Ecto.Schema
schema "products" do
field :name, :string
...
belongs_to(:shop, Shop)
end
end
If you want to be able to dynamically add products in a form, use the
dynamic_inputs_for
helper in combination with dynamic_add_button
to generate
the form.
If you also want to allow the deletion of nested fields, this library follows the
strategy suggested in the
Ecto.Changeset documentation. Add a
separate boolean virtual field to the changeset function that will allow you to
manually mark the associated data for deletion and use the dynamic_delete_button
helper inside the function that you pass to dynamic_inputs_for
to generate a delete
button for each associated data.
defmodule Product do
use Ecto.Schema
import Ecto.Changeset
schema "products" do
field :name, :string
...
field :delete, :boolean, virtual: true
belongs_to(:shop, Shop)
end
def changeset(product, params) do
product
|> cast(params, [:name, :delete])
|> maybe_mark_for_deletion
end
defp maybe_mark_for_deletion(changeset) do
if get_change(changeset, :delete) do
%{changeset | action: :delete}
else
changeset
end
end
end
<%= form_for @changeset, Routes.shop_path(@conn, :create), fn f -> %>
<%= text_input f, :name %>
<%= dynamic_inputs_for f, :products, %Product{}, fn f_product -> %>
<%= text_input f_product, :name %>
<%= dynamic_delete_button("Delete") %>
<% end%>
<%= dynamic_add_button :products, "Add" %>
<% end %>
If you want the new fields to have default values, you can pass them to the schema
you pass to dynamic_inputs_for
. In the previous example %Product{name: "ASDF"}
.
<%= dynamic_inputs_for f, :products, %Product{name: "ASDF"}, fn f_product -> %>
When you add or delete an element, the events dynamic:addedFields
and
dynamic:deletedFields
are triggered. These events can be listened to modify the
nested fields or integrate them with third party javascript libraries.
document.addEventListener(
"dynamic:addedFields",
function(e) {
e.target.style.backgroundColor = "red";
},
false
);
or if you use jQuery
$(document).on("dynamic:addedFields", e => {
e.target.style.backgroundColor = "red";
});