-
Notifications
You must be signed in to change notification settings - Fork 64
Use masked input for Backpex.Fields.Currency
#1307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
530097f
ffa9a6a
c52444d
502803b
f9e4374
c331680
2c8a149
49e319b
1e6e4ef
3d03c2d
90f039d
5b80804
4a56ecf
ba1bf96
1c81961
48ac227
caefd4b
fe1b3b1
5ca315e
b11c8d8
1b5c979
07cc077
7e81909
79c2ad0
f5f2118
fbd5d60
e2b3487
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'), '') | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align config keys:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: "," | ||
] | ||
] | ||
|
||
|
@@ -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 | ||
|
||
|
@@ -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> | ||
|
@@ -91,25 +111,7 @@ defmodule Backpex.Fields.Currency do | |
def search_condition(schema_name, field_name, search_string) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 WDYT? @krns @pehbehbeh There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have the same problem when using a range filter. The |
||
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) | ||
Flo0807 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
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 |
Uh oh!
There was an error while loading. Please reload this page.