Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
530097f
Replace custom amount type with money amount type
Flo0807 Jul 18, 2025
ffa9a6a
Add masked input
Flo0807 Jul 18, 2025
c52444d
Refactor currency field to use masked input
Flo0807 Jul 18, 2025
502803b
Remove money dependency from backpex
Flo0807 Jul 18, 2025
f9e4374
Install money in demo
Flo0807 Jul 18, 2025
c331680
Use hidden input for currency value
Flo0807 Jul 18, 2025
2c8a149
Update masked input
Flo0807 Aug 8, 2025
49e319b
Update js build
Flo0807 Aug 8, 2025
1e6e4ef
Rename masked_input to masked_number_input
Flo0807 Aug 8, 2025
3d03c2d
Remove debug message
Flo0807 Aug 8, 2025
90f039d
Remove binding
Flo0807 Aug 8, 2025
5b80804
Update default for `unit_position`
Flo0807 Aug 8, 2025
4a56ecf
Refactor number input to currency input
Flo0807 Aug 15, 2025
ba1bf96
Update lib/backpex/html/form.ex
Flo0807 Aug 22, 2025
1c81961
Update dependency igniter to v0.6.28 (#1428)
renovate[bot] Aug 22, 2025
48ac227
Update assets
Flo0807 Aug 22, 2025
caefd4b
Install backpex node dependencies
Flo0807 Aug 22, 2025
fe1b3b1
Merge branch 'develop' into feature/1280-improve-currency-field
Flo0807 Sep 19, 2025
5ca315e
Run `mix format`
Flo0807 Sep 19, 2025
b11c8d8
Merge branch 'develop' into feature/1280-improve-currency-field
Flo0807 Sep 19, 2025
1b5c979
Fix imask cannot be resolved
Flo0807 Sep 19, 2025
07cc077
Use USD as currency default
Flo0807 Sep 19, 2025
7e81909
Remove unused NODE_PATH env
Flo0807 Sep 19, 2025
79c2ad0
Install backpex deps
Flo0807 Sep 19, 2025
f5f2118
Add v0.16 upgrade guide
Flo0807 Sep 19, 2025
fbd5d60
Fix spelling mistake
Flo0807 Sep 26, 2025
e2b3487
Merge branch 'develop' into feature/1280-improve-currency-field
Flo0807 Oct 6, 2025
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 Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ ENV PATH=/opt/scripts/:/opt/app/_build/prod/rel/demo/bin:$PATH
ARG MIX_ENV=prod
ENV MIX_ENV=$MIX_ENV

# Install root-level (Backpex) dependencies
COPY package.json yarn.lock ./
RUN yarn install --pure-lockfile

RUN mkdir demo
WORKDIR $APP_HOME/demo

Expand Down
49 changes: 49 additions & 0 deletions assets/js/hooks/_currency_input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import IMask from 'imask'

export default {
mounted () {
this.maskedInput = this.el.querySelector('[data-masked-input]')
this.hiddenInput = this.el.querySelector('[data-hidden-input]')

this.initializeMask()
},
initializeMask () {
const maskPattern = this.el.dataset.maskPattern

if (!maskPattern) {
console.error('You must provide a mask pattern in the data-masked-pattern attribute.')
return
}

this.maskOptions = {
mask: maskPattern,
lazy: false,
blocks: {
num: {
mask: Number,
thousandsSeparator: this.el.dataset.thousandsSeparator,
radix: this.el.dataset.radix
}
}
}

this.mask = IMask(this.maskedInput, this.maskOptions)
this.mask.unmaskedValue = this.rawValue(this.hiddenInput.value)
this.mask.on('accept', this.handleMaskChange.bind(this))
},
updated () {
this.handleMaskChange()
},
handleMaskChange () {
this.hiddenInput.value = this.rawValue(this.mask.value)
},
destroyed () {
this.mask.destroy()
},
rawValue (value) {
return value
.replace(this.el.dataset.unit || '', '')
.trim()
.replace(new RegExp(`\\${this.el.dataset.thousandsSeparator}`, 'g'), '')
}
}
1 change: 1 addition & 0 deletions assets/js/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as BackpexSidebarSections } from './_sidebar_sections'
export { default as BackpexStickyActions } from './_sticky_actions'
export { default as BackpexThemeSelector } from './_theme_selector'
export { default as BackpexTooltip } from './_tooltip'
export { default as BackpexCurrencyInput } from './_currency_input'
1 change: 1 addition & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
- STARTUP_COMMAND_2=yarn install --pure-lockfile
- STARTUP_COMMAND_3=yarn playwright install chromium --with-deps
- STARTUP_COMMAND_4=mix assets.setup
- STARTUP_COMMAND_5=cd .. && mix deps.get && yarn install --pure-lockfile && cd demo
volumes:
- .:/opt/app
ports:
Expand Down
10 changes: 8 additions & 2 deletions demo/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,20 @@ config :esbuild,
],
backpex: [
args: ~w(../assets/js/backpex.js --bundle --format=esm --sourcemap --outfile=priv/static/js/backpex.esm.js),
cd: Path.expand("..", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
cd: Path.expand("..", __DIR__)
]

config :logger, :default_formatter,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]

config :money,
default_currency: :USD,
separator: ",",
delimiter: ".",
symbol_on_right: false,
symbol_space: true

config :phoenix, :json_library, Jason

config :sentry,
Expand Down
4 changes: 1 addition & 3 deletions demo/lib/demo/invoice.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ defmodule Demo.Invoice do
schema "invoices" do
field :company, :string

field :amount, Backpex.Ecto.Amount.Type,
currency: :EUR,
opts: [separator: ".", delimiter: ",", symbol_on_right: true, symbol_space: true]
field :amount, Money.Ecto.Amount.Type

timestamps()
end
Expand Down
4 changes: 1 addition & 3 deletions demo/lib/demo/product.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ defmodule Demo.Product do
field :manufacturer, :string
field :images, {:array, :string}

field :price, Backpex.Ecto.Amount.Type,
currency: :EUR,
opts: [separator: ".", delimiter: ",", symbol_on_right: true, symbol_space: true]
field :price, Money.Ecto.Amount.Type

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 Down
1 change: 1 addition & 0 deletions demo/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ defmodule Demo.MixProject do
{:jason, ">= 1.0.0"},
{:ash, "~> 3.0"},
{:ash_postgres, "~> 2.6.0"},
{:money, "~> 1.14"},

# assets
{:esbuild, "~> 0.9", runtime: Mix.env() == :dev},
Expand Down
61 changes: 61 additions & 0 deletions guides/upgrading/v0.16.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,67 @@ defp deps do
end
```

## `Backpex.Fields.Currency` has been updated

The appearance of the currency field has changed completely. We now use a custom masked input instead of a number input which improves UX a lot.
In addition we've removed the `money` dependency from Backpex so you can use whatever library you want to use for currencies in your app.
The removal of `money` includes our custom [`Backpex.Ecto.AmountType`]() ecto type.

If you used the `money` library before, these are the changes you have to make:

1. Configure `unit`, `unit_position`, `radix` and `thousands_separator` for `Backpex.Fields.Currency`

```elixir
def fields do
[
price: %{
module: Backpex.Fields.Currency,
label: "Price",
unit: "",
unit_position: :after,
radix: ",",
thousands_separator: "."
},
...
]
end
```

See [field-specific options](`Backpex.Fields.Currency`) for default values.

2. Configure defaults for `money` in your `config/config.exs`

```elixir
config :money,
default_currency: :EUR,
separator: ".",
delimiter: ",",
symbol_on_right: true,
symbol_space: true
Comment on lines +28 to +51
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align config keys:

  • separator vs. thousands_separator
  • delimiter vs. radix
  • symbol_on_right vs. unit_position
  • symbol_space not available on field level?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, got it.. thats the money config vs. backpex config.. hmhmh

```

The default values should ideally match the field options from step 1.

3. Replace [`Backpex.Ecto.AmountType`]() with [`Money.Ecto.Amount.Type`]()

```elixir
schema "products" do
field :price, Money.Ecto.Amount.Type
...
end
```

4. Add money dependency to your app

```elixir
def deps do
[
{:money, "~> 1.14"},
...
]
end
```

## Component changes

- `Backpex.HTML.Layout.field_container/1` now uses a `dl` element instead of a `div` element to better align with HTML semantics
Expand Down
61 changes: 0 additions & 61 deletions lib/backpex/ecto/amount_type.ex

This file was deleted.

80 changes: 41 additions & 39 deletions lib/backpex/fields/currency.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ defmodule Backpex.Fields.Currency do
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
],
unit: [
doc: "Unit to display with the currency value, e.g. '€'.",
type: :string,
default: "$"
],
unit_position: [
doc: "Position of the unit relative to the value, either `:before` or `:after`.",
type: {:in, [:before, :after]},
default: :before
],
radix: [
doc:
"Character used as the decimal separator, e.g. ',' or '.'. Make sure this value matches the one you've configured in your Money library.",
type: :string,
default: "."
],
thousands_separator: [
doc:
"Character used as the thousands separator, e.g. '.' or ','. Make sure this value matches the one you've configured in your Money library.",
type: :string,
default: ","
]
]

Expand All @@ -21,16 +43,14 @@ defmodule Backpex.Fields.Currency do

## Schema

`Backpex.Ecto.Amount.Type` provides a type for Ecto to store a amount. The underlying data type should be an integer.
For a full list of configuration options see: https://hexdocs.pm/money/Money.html#module-configuration
Backpex expects you to use a Money library or a similar approach for handling currency values and dumping / casting them correctly in your database schema.

schema "article" do
field :price, Backpex.Ecto.Amount.Type
...
end
Ensure that your schema field is set up to handle the currency type appropriately.

For example, if you are using the [Money](https://hex.pm/packages/money) library, your schema might look like this:

schema "article" do
field :price, Backpex.Ecto.Amount.Type, currency: :EUR, opts: [separator: ".", delimiter: ","]
field :price, Money.Ecto.Amount.Type
...
end

Expand All @@ -41,46 +61,46 @@ defmodule Backpex.Fields.Currency do
[
price: %{
module: Backpex.Fields.Currency,
label: "Price"
label: "Price",
unit: "€",
radix: ",",
thousands_separator: "."
}
]
end
"""
use Backpex.Field, config_schema: @config_schema

import Ecto.Query
alias Backpex.Ecto.Amount.Type

@impl Backpex.Field
def render_value(assigns) do
schema = assigns.live_resource.adapter_config(:schema)
assigns = assign(assigns, :casted_value, maybe_cast_value(assigns.name, schema, assigns.value))

~H"""
<p class={@live_action in [:index, :resource_action] && "truncate"}>
{@casted_value}
{@value}
</p>
"""
end

@impl Backpex.Field
def render_form(assigns) do
assigns = assign(assigns, :casted_value, maybe_cast_form(PhoenixForm.input_value(assigns.form, assigns.name)))

~H"""
<div>
<Layout.field_container>
<:label align={Backpex.Field.align_label(@field_options, assigns)}>
<Layout.input_label for={@form[@name]} text={@field_options[:label]} />
</:label>
<BackpexForm.input
type="number"
<BackpexForm.currency_input
type="text"
field={@form[@name]}
value={@casted_value}
translate_error_fun={Backpex.Field.translate_error_fun(@field_options, assigns)}
help_text={Backpex.Field.help_text(@field_options, assigns)}
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
step=".01"
min="0"
radix={@field_options[:radix]}
thousands_separator={@field_options[:thousands_separator]}
unit={@field_options[:unit]}
unit_position={@field_options[:unit_position]}
/>
</Layout.field_container>
</div>
Expand All @@ -91,25 +111,7 @@ defmodule Backpex.Fields.Currency do
def search_condition(schema_name, field_name, search_string) do
Copy link
Collaborator Author

@Flo0807 Flo0807 Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, the UX when searching for currency is not very good, as you have to search for integer values. We could add another field_option that is used in this function to convert the search string into an integer. (However, we would need to modify the function to receive the field options...)

WDYT? @krns @pehbehbeh

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the same apply for Filters like a Range filter?

To have some kind of options to transform the search term sounds good.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the same problem when using a range filter. The search_condition is not used there, so I guess we should leave it like it is and find a solution that covers both filters and field search.

dynamic(
[{^schema_name, schema_name}],
ilike(fragment("CAST(? AS TEXT)", schema_name |> field(^field_name)), ^search_string)
ilike(fragment("CAST(? AS TEXT)", field(schema_name, ^field_name)), ^search_string)
)
end

defp maybe_cast_value(field_name, schema, value) do
type = schema.__schema__(:type, field_name) || schema.__schema__(:virtual_type, field_name)

case type do
{:parameterized, Backpex.Ecto.Amount.Type, opts} ->
{:ok, money} = Type.cast(value, opts)

Money.to_string(money, Keyword.get(opts, :opts, []))

_type ->
value
end
end

defp maybe_cast_form(val) when is_binary(val), do: val
defp maybe_cast_form(nil), do: Decimal.new("0.00")
defp maybe_cast_form(%Money{} = value), do: Money.to_decimal(value)
end
Loading
Loading