Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions demo/lib/demo/ecto_factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ defmodule Demo.EctoFactory do
quantity: Enum.random(0..1_000),
manufacturer: "https://example.com/",
price: Enum.random(50..5_000_000),
more_info: %{
weight: Enum.random(1..100),
goes_well_with: Faker.Food.description()
},
suppliers: build_list(Enum.random(0..5), :supplier),
short_links: build_list(Enum.random(0..5), :short_link)
}
Expand Down
14 changes: 14 additions & 0 deletions demo/lib/demo/product.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ defmodule Demo.Product do

field :price, Money.Ecto.Amount.Type

embeds_one :more_info, MoreInfo do
field :weight, :integer
field :goes_well_with, :string
end

has_many :suppliers, Supplier, on_replace: :delete, on_delete: :delete_all
has_many :short_links, ShortLink, on_replace: :delete, on_delete: :delete_all, foreign_key: :product_id

Expand All @@ -29,6 +34,9 @@ defmodule Demo.Product do
def changeset(product, attrs, _metadata \\ []) do
product
|> cast(attrs, @required_fields ++ @optional_fields)
|> cast_embed(:more_info,
with: &more_info_changeset/2
)
|> cast_assoc(:suppliers,
with: &Demo.Supplier.changeset/2,
sort_param: :suppliers_order,
Expand All @@ -42,4 +50,10 @@ defmodule Demo.Product do
|> validate_required(@required_fields)
|> validate_length(:images, max: 2)
end

def more_info_changeset(more_info, attrs) do
more_info
|> cast(attrs, [:weight, :goes_well_with])
|> validate_required([:weight])
end
end
18 changes: 18 additions & 0 deletions demo/lib/demo_web/live/product_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ defmodule DemoWeb.ProductLive do
label: "Price",
align: :right
},
more_info: %{
module: Backpex.Fields.InlineCRUD,
label: "More Info",
type: :embed_one,
except: [:index],
child_fields: [
weight: %{
module: Backpex.Fields.Text,
label: "Ave. Weight (kg)"
},
goes_well_with: %{
module: Backpex.Fields.Textarea,
label: "Goes well with",
input_type: :textarea,
rows: 5
}
]
},
suppliers: %{
module: Backpex.Fields.InlineCRUD,
label: "Suppliers",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Demo.Repo.Migrations.ProductsAddMoreInfo do
use Ecto.Migration

def change do
alter table(:products) do
add :more_info, :map
end
end
end
81 changes: 49 additions & 32 deletions lib/backpex/fields/inline_crud.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
defmodule Backpex.Fields.InlineCRUD do
@config_schema [
type: [
doc: "The type of the field.",
type: {:in, [:embed, :assoc]},
doc: "The type of the field. One of `:embed`, `:embed_one` or `:assoc`.",
type: {:in, [:embed, :assoc, :embed_one]},
required: true
],
child_fields: [
Expand All @@ -20,7 +20,7 @@ defmodule Backpex.Fields.InlineCRUD do
]

@moduledoc """
A field to handle inline CRUD operations. It can be used with either an `embeds_many` or `has_many` (association) type column.
A field to handle inline CRUD operations. It can be used with either an `embeds_many`, `embeds_one`, or `has_many` (association) type column.

## Field-specific options

Expand All @@ -32,7 +32,7 @@ defmodule Backpex.Fields.InlineCRUD do
>
> Everything is currently handled by plain text input.

### EmbedsMany
### EmbedsMany and EmbedsOne

The field in the migration must be of type `:map`. You also need to use ecto's `cast_embed/2` in the changeset.

Expand All @@ -43,8 +43,8 @@ defmodule Backpex.Fields.InlineCRUD do
...
|> cast_embed(:your_field,
with: &your_field_changeset/2,
sort_param: :your_field_order,
drop_param: :your_field_delete
sort_param: :your_field_order, # not required for embeds_one
drop_param: :your_field_delete # not required for embeds_one
)
...
end
Expand Down Expand Up @@ -97,6 +97,13 @@ defmodule Backpex.Fields.InlineCRUD do

@impl Backpex.Field
def render_value(assigns) do
assigns =
assigns
|> assign(
:value,
if(assigns[:field_options].type == :embed_one, do: [get_value(assigns, :value)], else: assigns[:value])
)

~H"""
<div class="ring-base-content/10 rounded-box overflow-x-auto ring-1">
<table class="table">
Expand Down Expand Up @@ -157,34 +164,37 @@ defmodule Backpex.Fields.InlineCRUD do
phx-throttle={Backpex.Field.throttle(child_field_options, assigns)}
/>
</div>

<div class={if f_nested.index == 0, do: "mt-5", else: nil}>
<label for={"#{@name}-checkbox-delete-#{f_nested.index}"}>
<input
id={"#{@name}-checkbox-delete-#{f_nested.index}"}
type="checkbox"
name={"change[#{@name}_delete][]"}
value={f_nested.index}
class="hidden"
/>

<div class="btn btn-outline btn-error" aria-label={Backpex.__("Delete", @live_resource)}>
<Backpex.HTML.CoreComponents.icon name="hero-trash" class="h-5 w-5" />
</div>
</label>
</div>
<%= if @field_options.type != :embed_one do %>
<div class={if f_nested.index == 0, do: "mt-5", else: nil}>
<label for={"#{@name}-checkbox-delete-#{f_nested.index}"}>
<input
id={"#{@name}-checkbox-delete-#{f_nested.index}"}
type="checkbox"
name={"change[#{@name}_delete][]"}
value={f_nested.index}
class="hidden"
/>

<div class="btn btn-outline btn-error" aria-label={Backpex.__("Delete", @live_resource)}>
<Backpex.HTML.CoreComponents.icon name="hero-trash" class="h-5 w-5" />
</div>
</label>
</div>
<% end %>
</div>
</.inputs_for>

<input type="hidden" name={"change[#{@name}_delete][]"} />
<%= if @field_options.type != :embed_one do %>
<input type="hidden" name={"change[#{@name}_delete][]"} />
<% end %>
</div>
<input
name={"change[#{@name}_order][]"}
type="checkbox"
aria-label={Backpex.__("Add entry", @live_resource)}
class="btn btn-outline btn-sm btn-primary"
/>

<%= if @field_options.type != :embed_one do %>
<input
name={"change[#{@name}_order][]"}
type="checkbox"
aria-label={Backpex.__("Add entry", @live_resource)}
class="btn btn-outline btn-sm btn-primary"
/>
<% end %>
<%= if help_text = Backpex.Field.help_text(@field_options, assigns) do %>
<Backpex.HTML.Form.help_text class="mt-1">{help_text}</Backpex.HTML.Form.help_text>
<% end %>
Expand All @@ -195,7 +205,7 @@ defmodule Backpex.Fields.InlineCRUD do

@impl Backpex.Field
def association?({_name, %{type: :assoc}} = _field), do: true
def association?({_name, %{type: :embed}} = _field), do: false
def association?({_name, %{type: _type}} = _field), do: false

@impl Backpex.Field
def schema({name, _field_options}, schema) do
Expand All @@ -211,4 +221,11 @@ defmodule Backpex.Fields.InlineCRUD do
do: input_type

defp input_type(_child_field_options), do: :text

defp get_value(assigns, field) do
case Map.get(assigns, field, %{}) do
nil -> %{}
value -> value
end
end
end
Loading