Skip to content

Commit beb5842

Browse files
committed
Add masked input
1 parent e4d95d7 commit beb5842

File tree

5 files changed

+137
-0
lines changed

5 files changed

+137
-0
lines changed

assets/js/hooks/_masked_input.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import IMask from 'imask'
2+
3+
export default {
4+
mounted () {
5+
this.maskedInput = this.el.querySelector('[data-masked-input]')
6+
this.hiddenInput = this.el.querySelector('[data-hidden-input]')
7+
8+
this.initializeMask()
9+
},
10+
initializeMask () {
11+
const maskPattern = this.el.dataset.unit ? `num ${this.el.dataset.unit}` : 'num'
12+
13+
this.maskOptions = {
14+
mask: maskPattern,
15+
lazy: false,
16+
blocks: {
17+
num: {
18+
mask: Number,
19+
thousandsSeparator: this.el.dataset.thousandsSeparator,
20+
radix: this.el.dataset.radix
21+
}
22+
}
23+
}
24+
25+
this.mask = IMask(this.maskedInput, this.maskOptions)
26+
},
27+
destroyed () {
28+
this.mask.destroy()
29+
}
30+
}

assets/js/hooks/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { default as BackpexSidebarSections } from './_sidebar_sections'
44
export { default as BackpexStickyActions } from './_sticky_actions'
55
export { default as BackpexThemeSelector } from './_theme_selector'
66
export { default as BackpexTooltip } from './_tooltip'
7+
export { default as BackpexMaskedInput } from './_masked_input'

lib/backpex/html/form.ex

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,90 @@ defmodule Backpex.HTML.Form do
164164
"""
165165
end
166166

167+
@doc """
168+
Renders a masked input.
169+
170+
A `Phoenix.HTML.FormField` may be passed as argument, which is used to retrieve the input name, id, and values.
171+
Otherwise all attributes may be passed explicitly.
172+
173+
## Examples
174+
175+
<.masked_input field={@form[:amount]} unit="€" />
176+
<.masked_input id="amount-input" name="amount" value="20" unit="€" readonly />
177+
"""
178+
attr :id, :any, default: nil
179+
attr :name, :any
180+
attr :label, :string, default: nil
181+
attr :help_text, :string, default: nil
182+
attr :value, :any
183+
184+
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]"
185+
186+
attr :errors, :list, default: []
187+
188+
attr :class, :any, default: nil, doc: "additional classes for the container element"
189+
190+
attr :input_class, :any,
191+
default: nil,
192+
doc: "the input class to use over defaults, note that this is applied to a wrapper span element
193+
to allow for proper styling of the masked input"
194+
195+
attr :error_class, :any, default: nil, doc: "the input error class to use over defaults"
196+
197+
attr :translate_error_fun, :any, default: &Function.identity/1, doc: "a custom function to map form errors"
198+
attr :hide_errors, :boolean, default: false, doc: "if errors should be hidden"
199+
200+
attr :unit, :string, default: nil
201+
attr :radix, :string, default: "."
202+
attr :thousands_separator, :string, default: ","
203+
204+
attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
205+
multiple pattern placeholder readonly required rows size step)
206+
207+
def masked_input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
208+
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
209+
210+
assigns
211+
|> assign(field: nil, id: assigns.id || field.id)
212+
|> assign(:errors, translate_form_errors(errors, assigns.translate_error_fun))
213+
|> assign_new(:name, fn -> field.name end)
214+
|> assign_new(:value, fn -> field.value end)
215+
|> masked_input()
216+
end
217+
218+
def masked_input(assigns) do
219+
~H"""
220+
<div class={["fieldset py-0", @class]}>
221+
<span :if={@label} class="label mb-1">{@label}</span>
222+
<div
223+
id={@id}
224+
phx-hook="BackpexMaskedInput"
225+
data-radix={@radix}
226+
data-unit={@unit}
227+
data-thousands-separator={@thousands_separator}
228+
>
229+
<%!-- As the input ignores updates, we need to wrap it in a span to apply the styles correctly --%>
230+
<span class={[
231+
@input_class || "[&_>_input]:input [&_>_input]:w-full",
232+
@errors != [] && (@error_class || "[&_>_input]:input-error [&_>_input]:bg-error/10")
233+
]}>
234+
<input
235+
id={"#{@id}_masked"}
236+
name={@name}
237+
value={@value}
238+
data-masked-input
239+
phx-update="ignore"
240+
class={@input_class}
241+
{@rest}
242+
/>
243+
</span>
244+
</div>
245+
<.error :for={msg <- @errors} :if={not @hide_errors}>{msg}</.error>
246+
<.help_text :if={@help_text}>{@help_text}</.help_text>
247+
</div>
248+
"""
249+
end
250+
167251
@doc """
168252
Generates a generic error message.
169253
"""

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,8 @@
3535
},
3636
"devDependencies": {
3737
"standard": "17.1.2"
38+
},
39+
"dependencies": {
40+
"imask": "^7.6.1"
3841
}
3942
}

yarn.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
# yarn lockfile v1
33

44

5+
"@babel/runtime-corejs3@^7.24.4":
6+
version "7.28.0"
7+
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.28.0.tgz#4d9938897f5a9aaa9e5f99408a4bf86daba40ec1"
8+
integrity sha512-nlIXnSqLcBij8K8TtkxbBJgfzfvi75V1pAKSM7dUXejGw12vJAqez74jZrHTsJ3Z+Aczc5Q/6JgNjKRMsVU44g==
9+
dependencies:
10+
core-js-pure "^3.43.0"
11+
512
"@eslint-community/eslint-utils@^4.2.0":
613
version "4.7.0"
714
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
@@ -305,6 +312,11 @@ [email protected]:
305312
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
306313
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
307314

315+
core-js-pure@^3.43.0:
316+
version "3.44.0"
317+
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.44.0.tgz#6e9d6c128c8b967c5eac4f181c2b654d85c28090"
318+
integrity sha512-gvMQAGB4dfVUxpYD0k3Fq8J+n5bB6Ytl15lqlZrOIXFzxOhtPaObfkQGHtMRdyjIf7z2IeNULwi1jEwyS+ltKQ==
319+
308320
cross-spawn@^7.0.2:
309321
version "7.0.6"
310322
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -981,6 +993,13 @@ ignore@^5.1.1, ignore@^5.2.0:
981993
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
982994
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
983995

996+
imask@^7.6.1:
997+
version "7.6.1"
998+
resolved "https://registry.yarnpkg.com/imask/-/imask-7.6.1.tgz#04fa4693bf47a4a71bbf7325408e0d058a74dcad"
999+
integrity sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==
1000+
dependencies:
1001+
"@babel/runtime-corejs3" "^7.24.4"
1002+
9841003
import-fresh@^3.2.1:
9851004
version "3.3.1"
9861005
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"

0 commit comments

Comments
 (0)