phlex-forms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/app/javascript/phlex_forms/controllers/validations/acceptance_controller.js +23 -0
- data/app/javascript/phlex_forms/controllers/validations/base_controller.js +166 -0
- data/app/javascript/phlex_forms/controllers/validations/confirmation_controller.js +27 -0
- data/app/javascript/phlex_forms/controllers/validations/exclusion_controller.js +19 -0
- data/app/javascript/phlex_forms/controllers/validations/form_controller.js +43 -0
- data/app/javascript/phlex_forms/controllers/validations/format_controller.js +23 -0
- data/app/javascript/phlex_forms/controllers/validations/inclusion_controller.js +19 -0
- data/app/javascript/phlex_forms/controllers/validations/length_controller.js +95 -0
- data/app/javascript/phlex_forms/controllers/validations/numericality_controller.js +59 -0
- data/app/javascript/phlex_forms/controllers/validations/presence_controller.js +14 -0
- data/app/javascript/phlex_forms/i18n.js +40 -0
- data/app/javascript/phlex_forms/messages.js +100 -0
- data/config/importmap.rb +21 -0
- data/config/locales/de.yml +12 -0
- data/config/locales/en.yml +14 -0
- data/config/locales/sv.yml +12 -0
- data/config/rubocop.yml +20 -0
- data/lib/forms/checkbox.rb +39 -0
- data/lib/forms/choices_select.rb +132 -0
- data/lib/forms/collection_check_box.rb +26 -0
- data/lib/forms/collection_check_box_builder.rb +26 -0
- data/lib/forms/collection_label.rb +20 -0
- data/lib/forms/email_field.rb +11 -0
- data/lib/forms/field.rb +222 -0
- data/lib/forms/field_error.rb +26 -0
- data/lib/forms/field_hint.rb +24 -0
- data/lib/forms/fields_for_builder.rb +58 -0
- data/lib/forms/file_input.rb +29 -0
- data/lib/forms/form.rb +225 -0
- data/lib/forms/form_control.rb +42 -0
- data/lib/forms/input.rb +35 -0
- data/lib/forms/label.rb +32 -0
- data/lib/forms/password_field.rb +11 -0
- data/lib/forms/radio.rb +29 -0
- data/lib/forms/range.rb +33 -0
- data/lib/forms/rich_textarea.rb +43 -0
- data/lib/forms/select.rb +73 -0
- data/lib/forms/submit.rb +62 -0
- data/lib/forms/textarea.rb +27 -0
- data/lib/forms/time_zone_select.rb +52 -0
- data/lib/forms/toggle.rb +39 -0
- data/lib/forms/validations/introspector.rb +224 -0
- data/lib/forms/validations/manual_rules.rb +77 -0
- data/lib/forms/wrapped_input.rb +67 -0
- data/lib/phlex-forms.rb +4 -0
- data/lib/phlex_forms/builder.rb +153 -0
- data/lib/phlex_forms/class_merge.rb +56 -0
- data/lib/phlex_forms/configuration.rb +49 -0
- data/lib/phlex_forms/delegated_field.rb +51 -0
- data/lib/phlex_forms/engine.rb +35 -0
- data/lib/phlex_forms/inline_icons.rb +36 -0
- data/lib/phlex_forms/rubocop.rb +14 -0
- data/lib/phlex_forms/version.rb +5 -0
- data/lib/phlex_forms.rb +80 -0
- data/lib/rubocop/phlex_forms/cop/legacy_form_method.rb +103 -0
- data/lib/rubocop/phlex_forms/cop/raw_form.rb +51 -0
- metadata +181 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
+
import { t } from "phlex_forms/i18n"
|
|
3
|
+
|
|
4
|
+
// Mirrors ActiveModel::Validations::NumericalityValidator.
|
|
5
|
+
export default class extends FieldValidatorController {
|
|
6
|
+
static values = {
|
|
7
|
+
greaterThan: Number,
|
|
8
|
+
greaterThanOrEqualTo: Number,
|
|
9
|
+
lessThan: Number,
|
|
10
|
+
lessThanOrEqualTo: Number,
|
|
11
|
+
equalTo: Number,
|
|
12
|
+
otherThan: Number,
|
|
13
|
+
onlyInteger: { type: Boolean, default: false },
|
|
14
|
+
odd: { type: Boolean, default: false },
|
|
15
|
+
even: { type: Boolean, default: false },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
check(value) {
|
|
19
|
+
const raw = String(value ?? "").trim()
|
|
20
|
+
if (raw === "") return null
|
|
21
|
+
|
|
22
|
+
const number = Number(raw)
|
|
23
|
+
if (Number.isNaN(number)) return t("js.forms.validations.numericality.not_a_number")
|
|
24
|
+
|
|
25
|
+
if (this.onlyIntegerValue && !Number.isInteger(number)) {
|
|
26
|
+
return t("js.forms.validations.numericality.not_an_integer")
|
|
27
|
+
}
|
|
28
|
+
if (this.hasGreaterThanValue && !(number > this.greaterThanValue)) {
|
|
29
|
+
return t("js.forms.validations.numericality.greater_than", { count: this.greaterThanValue })
|
|
30
|
+
}
|
|
31
|
+
if (this.hasGreaterThanOrEqualToValue && !(number >= this.greaterThanOrEqualToValue)) {
|
|
32
|
+
return t("js.forms.validations.numericality.greater_than_or_equal_to", {
|
|
33
|
+
count: this.greaterThanOrEqualToValue,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
if (this.hasLessThanValue && !(number < this.lessThanValue)) {
|
|
37
|
+
return t("js.forms.validations.numericality.less_than", { count: this.lessThanValue })
|
|
38
|
+
}
|
|
39
|
+
if (this.hasLessThanOrEqualToValue && !(number <= this.lessThanOrEqualToValue)) {
|
|
40
|
+
return t("js.forms.validations.numericality.less_than_or_equal_to", {
|
|
41
|
+
count: this.lessThanOrEqualToValue,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
if (this.hasEqualToValue && number !== this.equalToValue) {
|
|
45
|
+
return t("js.forms.validations.numericality.equal_to", { count: this.equalToValue })
|
|
46
|
+
}
|
|
47
|
+
if (this.hasOtherThanValue && number === this.otherThanValue) {
|
|
48
|
+
return t("js.forms.validations.numericality.other_than", { count: this.otherThanValue })
|
|
49
|
+
}
|
|
50
|
+
// Rails' NumericalityValidator coerces with `.to_i` before
|
|
51
|
+
// checking parity, so `2.5` is treated as `2` (even). JS `%` on
|
|
52
|
+
// a float diverges from that — truncate first to keep client and
|
|
53
|
+
// server in agreement.
|
|
54
|
+
const truncated = Math.trunc(number)
|
|
55
|
+
if (this.oddValue && truncated % 2 === 0) return t("js.forms.validations.numericality.odd")
|
|
56
|
+
if (this.evenValue && truncated % 2 !== 0) return t("js.forms.validations.numericality.even")
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
+
import { t } from "phlex_forms/i18n"
|
|
3
|
+
|
|
4
|
+
// Mirrors ActiveModel::Validations::PresenceValidator.
|
|
5
|
+
export default class extends FieldValidatorController {
|
|
6
|
+
static values = {
|
|
7
|
+
required: { type: Boolean, default: true },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
check(value) {
|
|
11
|
+
if (!this.requiredValue) return null
|
|
12
|
+
return this.isBlank(value) ? t("js.forms.validations.presence.blank") : null
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Minimal, self-contained i18n for phlex-forms' client-side validation
|
|
2
|
+
// messages. Bundles the validation strings for every shipped locale so the
|
|
3
|
+
// gem needs no host i18n runtime. Host apps can override a message by defining
|
|
4
|
+
// window.PhlexForms.messages[locale] before the controllers connect.
|
|
5
|
+
//
|
|
6
|
+
// Locale is read from <html lang> (falling back to a <meta name="locale">, then
|
|
7
|
+
// "en"), matching the convention Rails apps already set.
|
|
8
|
+
|
|
9
|
+
import { messages } from "phlex_forms/messages"
|
|
10
|
+
|
|
11
|
+
function detectLocale() {
|
|
12
|
+
if (typeof document === "undefined") return "en"
|
|
13
|
+
const htmlLang = document.documentElement.lang
|
|
14
|
+
if (htmlLang) return htmlLang.split("-")[0]
|
|
15
|
+
const meta = document.querySelector('meta[name="locale"]')?.content
|
|
16
|
+
return meta ? meta.split("-")[0] : "en"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function overrides() {
|
|
20
|
+
if (typeof window === "undefined") return {}
|
|
21
|
+
return (window.PhlexForms && window.PhlexForms.messages) || {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function lookup(scope, locale) {
|
|
25
|
+
const table = { ...(messages[locale] || {}), ...(overrides()[locale] || {}) }
|
|
26
|
+
return scope.split(".").reduce((node, key) => (node == null ? undefined : node[key]), table)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function interpolate(string, vars) {
|
|
30
|
+
return string.replace(/%\{(\w+)\}/g, (_, key) => (key in vars ? String(vars[key]) : `%{${key}}`))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// t("js.forms.validations.presence.blank", { count: 3 })
|
|
34
|
+
// Falls back to English, then to the raw key, so a missing translation never
|
|
35
|
+
// throws or renders blank.
|
|
36
|
+
export function t(scope, vars = {}) {
|
|
37
|
+
const locale = detectLocale()
|
|
38
|
+
const message = lookup(scope, locale) ?? lookup(scope, "en") ?? scope
|
|
39
|
+
return typeof message === "string" ? interpolate(message, vars) : scope
|
|
40
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Bundled validation messages for phlex-forms, keyed by locale then by the
|
|
2
|
+
// `js.forms.validations.*` dotted scope. Combined from the Cosmos (en/sv/de) and
|
|
3
|
+
// getzazu/app (en/fr/af) locale sets; sv/de start from English and are refined
|
|
4
|
+
// by the locallingo translation pass. Host apps override via
|
|
5
|
+
// window.PhlexForms.messages.
|
|
6
|
+
export const messages = {
|
|
7
|
+
en: {
|
|
8
|
+
js: {
|
|
9
|
+
forms: {
|
|
10
|
+
validations: {
|
|
11
|
+
acceptance: { accepted: "must be accepted" },
|
|
12
|
+
confirmation: { confirmation: "doesn't match confirmation" },
|
|
13
|
+
exclusion: { exclusion: "is reserved" },
|
|
14
|
+
format: { invalid: "is invalid" },
|
|
15
|
+
inclusion: { inclusion: "is not included in the list" },
|
|
16
|
+
length: {
|
|
17
|
+
too_long: "is too long (maximum is %{count} characters)",
|
|
18
|
+
too_short: "is too short (minimum is %{count} characters)",
|
|
19
|
+
wrong_length: "is the wrong length (should be %{count} characters)",
|
|
20
|
+
},
|
|
21
|
+
numericality: {
|
|
22
|
+
equal_to: "must be equal to %{count}",
|
|
23
|
+
even: "must be even",
|
|
24
|
+
greater_than: "must be greater than %{count}",
|
|
25
|
+
greater_than_or_equal_to: "must be greater than or equal to %{count}",
|
|
26
|
+
less_than: "must be less than %{count}",
|
|
27
|
+
less_than_or_equal_to: "must be less than or equal to %{count}",
|
|
28
|
+
not_a_number: "is not a number",
|
|
29
|
+
not_an_integer: "must be an integer",
|
|
30
|
+
odd: "must be odd",
|
|
31
|
+
other_than: "must be other than %{count}",
|
|
32
|
+
},
|
|
33
|
+
presence: { blank: "can't be blank" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
fr: {
|
|
39
|
+
js: {
|
|
40
|
+
forms: {
|
|
41
|
+
validations: {
|
|
42
|
+
acceptance: { accepted: "doit être accepté(e)" },
|
|
43
|
+
confirmation: { confirmation: "ne concorde pas avec la confirmation" },
|
|
44
|
+
exclusion: { exclusion: "n'est pas disponible" },
|
|
45
|
+
format: { invalid: "n'est pas valide" },
|
|
46
|
+
inclusion: { inclusion: "n'est pas inclus(e) dans la liste" },
|
|
47
|
+
length: {
|
|
48
|
+
too_long: "est trop long (pas plus de %{count} caractères)",
|
|
49
|
+
too_short: "est trop court (au moins %{count} caractères)",
|
|
50
|
+
wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)",
|
|
51
|
+
},
|
|
52
|
+
numericality: {
|
|
53
|
+
equal_to: "doit être égal à %{count}",
|
|
54
|
+
even: "doit être pair",
|
|
55
|
+
greater_than: "doit être supérieur à %{count}",
|
|
56
|
+
greater_than_or_equal_to: "doit être supérieur ou égal à %{count}",
|
|
57
|
+
less_than: "doit être inférieur à %{count}",
|
|
58
|
+
less_than_or_equal_to: "doit être inférieur ou égal à %{count}",
|
|
59
|
+
not_a_number: "n'est pas un nombre",
|
|
60
|
+
not_an_integer: "doit être un nombre entier",
|
|
61
|
+
odd: "doit être impair",
|
|
62
|
+
other_than: "doit être différent de %{count}",
|
|
63
|
+
},
|
|
64
|
+
presence: { blank: "doit être rempli(e)" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
af: {
|
|
70
|
+
js: {
|
|
71
|
+
forms: {
|
|
72
|
+
validations: {
|
|
73
|
+
acceptance: { accepted: "moet aanvaar word" },
|
|
74
|
+
confirmation: { confirmation: "pas nie by bevestiging nie" },
|
|
75
|
+
exclusion: { exclusion: "is bespreek" },
|
|
76
|
+
format: { invalid: "is ongeldig" },
|
|
77
|
+
inclusion: { inclusion: "is nie by die lys ingesluit nie" },
|
|
78
|
+
length: {
|
|
79
|
+
too_long: "is te lank (maksimum is %{count} karakters)",
|
|
80
|
+
too_short: "is te kort (minimum is %{count} karakters)",
|
|
81
|
+
wrong_length: "is die verkeerde lengte (moet %{count} karakters wees)",
|
|
82
|
+
},
|
|
83
|
+
numericality: {
|
|
84
|
+
equal_to: "moet gelyk wees aan %{count}",
|
|
85
|
+
even: "moet ewe wees",
|
|
86
|
+
greater_than: "moet meer wees as %{count}",
|
|
87
|
+
greater_than_or_equal_to: "moet meer of gelykstaande wees aan %{count}",
|
|
88
|
+
less_than: "moet minder wees as %{count}",
|
|
89
|
+
less_than_or_equal_to: "moet minder of gelykstaande wees aan %{count}",
|
|
90
|
+
not_a_number: "is nie 'n getal nie",
|
|
91
|
+
not_an_integer: "moet 'n heelgetal wees",
|
|
92
|
+
odd: "moet onewe wees",
|
|
93
|
+
other_than: "moet anders wees as %{count}",
|
|
94
|
+
},
|
|
95
|
+
presence: { blank: "mag nie leeg wees nie" },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Auto-pins the gem's bundled Stimulus controllers for importmap-rails consumers.
|
|
4
|
+
# Register them in the host app (lazy loading recommended):
|
|
5
|
+
#
|
|
6
|
+
# // app/javascript/controllers/index.js
|
|
7
|
+
# import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
8
|
+
# lazyLoadControllersFrom("phlex_forms/controllers", application)
|
|
9
|
+
#
|
|
10
|
+
# The `choices` controller expects the `choices.js` package to be available in
|
|
11
|
+
# the host importmap (peer dependency); document it in the host README.
|
|
12
|
+
pin_all_from PhlexForms::Engine.root.join("app/javascript/phlex_forms/controllers"),
|
|
13
|
+
under: "phlex_forms/controllers",
|
|
14
|
+
to: "phlex_forms/controllers"
|
|
15
|
+
|
|
16
|
+
# The client-side validation controllers import the gem's own i18n helper and
|
|
17
|
+
# bundled messages; pin those modules so importmap can resolve them.
|
|
18
|
+
pin "phlex_forms/i18n",
|
|
19
|
+
to: "phlex_forms/i18n.js"
|
|
20
|
+
pin "phlex_forms/messages",
|
|
21
|
+
to: "phlex_forms/messages.js"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
de:
|
|
2
|
+
cmd:
|
|
3
|
+
create: Erstellen
|
|
4
|
+
create_model: "%{model} erstellen"
|
|
5
|
+
update: Aktualisieren
|
|
6
|
+
update_model: "%{model} aktualisieren"
|
|
7
|
+
components:
|
|
8
|
+
searchable_select:
|
|
9
|
+
no_results: Keine Ergebnisse gefunden
|
|
10
|
+
placeholder: Auswählen...
|
|
11
|
+
search_placeholder: Suchen...
|
|
12
|
+
no_choices: Keine Auswahl verfügbar
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
en:
|
|
2
|
+
# Default submit-button text for Forms::Submit. Host apps override by defining
|
|
3
|
+
# the same keys (their locale files load after the gem's).
|
|
4
|
+
cmd:
|
|
5
|
+
create: Create
|
|
6
|
+
create_model: Create %{model}
|
|
7
|
+
update: Update
|
|
8
|
+
update_model: Update %{model}
|
|
9
|
+
components:
|
|
10
|
+
searchable_select:
|
|
11
|
+
no_results: No results found
|
|
12
|
+
placeholder: Select...
|
|
13
|
+
search_placeholder: Search...
|
|
14
|
+
no_choices: No choices to choose from
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
sv:
|
|
2
|
+
cmd:
|
|
3
|
+
create: Skapa
|
|
4
|
+
create_model: Skapa %{model}
|
|
5
|
+
update: Uppdatera
|
|
6
|
+
update_model: Uppdatera %{model}
|
|
7
|
+
components:
|
|
8
|
+
searchable_select:
|
|
9
|
+
no_results: Inga resultat hittades
|
|
10
|
+
placeholder: Välj...
|
|
11
|
+
search_placeholder: Sök...
|
|
12
|
+
no_choices: Inga alternativ att välja
|
data/config/rubocop.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Shareable RuboCop config for phlex-forms' cops. Inherit it in a host app:
|
|
2
|
+
#
|
|
3
|
+
# require:
|
|
4
|
+
# - phlex_forms/rubocop
|
|
5
|
+
# inherit_gem:
|
|
6
|
+
# phlex-forms: config/rubocop.yml
|
|
7
|
+
#
|
|
8
|
+
# Both cops target Phlex view/component files by default; adjust Include in the
|
|
9
|
+
# host if your components live elsewhere.
|
|
10
|
+
PhlexForms/RawForm:
|
|
11
|
+
Enabled: true
|
|
12
|
+
Include:
|
|
13
|
+
- "app/components/**/*.rb"
|
|
14
|
+
- "app/views/**/*.rb"
|
|
15
|
+
|
|
16
|
+
PhlexForms/LegacyFormMethod:
|
|
17
|
+
Enabled: true
|
|
18
|
+
Include:
|
|
19
|
+
- "app/components/**/*.rb"
|
|
20
|
+
- "app/views/**/*.rb"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
# A model-bound checkbox, delegating to DaisyUI::Checkbox. Emits a hidden field
|
|
5
|
+
# carrying the unchecked value so an unchecked box still submits (the Rails
|
|
6
|
+
# checkbox convention).
|
|
7
|
+
class Checkbox < Phlex::HTML
|
|
8
|
+
include PhlexForms::DelegatedField
|
|
9
|
+
|
|
10
|
+
def initialize(*modifiers, name: nil, id: nil, value: "1", unchecked_value: "0",
|
|
11
|
+
checked: false, error: false, disabled: false, required: false, **attributes)
|
|
12
|
+
@modifiers = normalize_modifiers(modifiers)
|
|
13
|
+
@name = name
|
|
14
|
+
@id = id
|
|
15
|
+
@value = value
|
|
16
|
+
@unchecked_value = unchecked_value
|
|
17
|
+
@checked = checked
|
|
18
|
+
@error = error
|
|
19
|
+
@disabled = disabled
|
|
20
|
+
@required = required
|
|
21
|
+
@full_width = false
|
|
22
|
+
@attributes = attributes
|
|
23
|
+
super()
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def view_template
|
|
27
|
+
input(type: "hidden", name: @name, value: @unchecked_value) if @unchecked_value && @name
|
|
28
|
+
render DaisyUI::Checkbox.new(*daisy_modifiers, value: @value, **checkbox_attributes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def checkbox_attributes
|
|
34
|
+
attrs = binding_attributes
|
|
35
|
+
attrs[:checked] = true if @checked
|
|
36
|
+
attrs
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
# A searchable/multi-select `<select>` enhanced by the choices.js Stimulus
|
|
5
|
+
# controller (shipped with the gem). Renders a normal `<select>` server-side;
|
|
6
|
+
# the `choices` controller upgrades it on connect. choices.js replaces the
|
|
7
|
+
# element, so size/color positional modifiers are passed as data values and
|
|
8
|
+
# applied to the choices.js wrapper by the controller (not as daisyui classes).
|
|
9
|
+
class ChoicesSelect < Phlex::HTML
|
|
10
|
+
SIZE_MODIFIERS = %i[xs sm md lg xl].freeze
|
|
11
|
+
COLOR_MODIFIERS = %i[primary secondary accent neutral info success warning error].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(*modifiers, name: nil, id: nil, choices: [], selected: nil, multiple: false,
|
|
14
|
+
searchable: false, remove_item_button: nil, placeholder: nil,
|
|
15
|
+
include_blank: false, prompt: nil, error: false, disabled: false, required: false, **options)
|
|
16
|
+
@modifiers = modifiers
|
|
17
|
+
@name = name
|
|
18
|
+
@id = id
|
|
19
|
+
@choices = choices
|
|
20
|
+
@selected = selected
|
|
21
|
+
@multiple = multiple
|
|
22
|
+
@searchable = searchable
|
|
23
|
+
@remove_item_button = remove_item_button.nil? ? multiple : remove_item_button
|
|
24
|
+
@placeholder = placeholder
|
|
25
|
+
@include_blank = include_blank
|
|
26
|
+
@prompt = prompt
|
|
27
|
+
@error = error
|
|
28
|
+
@disabled = disabled
|
|
29
|
+
@required = required
|
|
30
|
+
@options = options
|
|
31
|
+
super()
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def view_template
|
|
35
|
+
select(**attrs) do
|
|
36
|
+
render_prompt if @prompt || @include_blank
|
|
37
|
+
render_choices
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def attrs
|
|
44
|
+
a = {
|
|
45
|
+
name: select_name,
|
|
46
|
+
id: @id,
|
|
47
|
+
class: PhlexForms::ClassMerge.merge("choices-select w-full", @options[:class]),
|
|
48
|
+
data: stimulus_data,
|
|
49
|
+
**@options.except(:class, :value, :data)
|
|
50
|
+
}
|
|
51
|
+
a[:multiple] = true if @multiple
|
|
52
|
+
a[:disabled] = true if @disabled
|
|
53
|
+
a[:required] = true if @required
|
|
54
|
+
a.compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def select_name
|
|
58
|
+
return nil unless @name
|
|
59
|
+
|
|
60
|
+
@multiple ? "#{@name}[]" : @name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stimulus_data
|
|
64
|
+
base = @options[:data] || {}
|
|
65
|
+
{
|
|
66
|
+
controller: [base[:controller], "choices"].compact.join(" "),
|
|
67
|
+
choices_searchable_value: @searchable,
|
|
68
|
+
choices_remove_item_button_value: @remove_item_button,
|
|
69
|
+
choices_placeholder_value: @placeholder.to_s,
|
|
70
|
+
choices_size_value: size_value,
|
|
71
|
+
choices_color_value: color_value,
|
|
72
|
+
**base.except(:controller)
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def size_value
|
|
77
|
+
(@modifiers & SIZE_MODIFIERS).first.to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def color_value
|
|
81
|
+
((@modifiers & COLOR_MODIFIERS).first || :primary).to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_prompt
|
|
85
|
+
text = @prompt || (@include_blank.is_a?(String) ? @include_blank : "")
|
|
86
|
+
option(value: "", selected: blank_selected?) { text }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def blank_selected?
|
|
90
|
+
@multiple ? Array(@selected).empty? : @selected.blank?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def render_choices
|
|
94
|
+
case @choices
|
|
95
|
+
when Hash then render_hash_choices(@choices)
|
|
96
|
+
else render_array_choices(Array(@choices))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_array_choices(choices)
|
|
101
|
+
choices.each do |choice|
|
|
102
|
+
if choice.is_a?(Array)
|
|
103
|
+
render_option(choice[1], choice[0])
|
|
104
|
+
else
|
|
105
|
+
render_option(choice, choice)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def render_hash_choices(choices)
|
|
111
|
+
choices.each do |label, value|
|
|
112
|
+
if value.is_a?(Array) || value.is_a?(Hash)
|
|
113
|
+
optgroup(label:) { value.is_a?(Array) ? render_array_choices(value) : render_hash_choices(value) }
|
|
114
|
+
else
|
|
115
|
+
render_option(value, label)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_option(value, label)
|
|
121
|
+
option(value: value.to_s, selected: selected?(value) || nil) { label.to_s }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def selected?(value)
|
|
125
|
+
if @multiple
|
|
126
|
+
Array(@selected).map(&:to_s).include?(value.to_s)
|
|
127
|
+
else
|
|
128
|
+
@selected.to_s == value.to_s
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
# A single checkbox within a collection_check_boxes group.
|
|
5
|
+
class CollectionCheckBox < Phlex::HTML
|
|
6
|
+
def initialize(name:, id:, value:, checked:, **options)
|
|
7
|
+
@name = name
|
|
8
|
+
@id = id
|
|
9
|
+
@value = value
|
|
10
|
+
@checked = checked
|
|
11
|
+
@options = options
|
|
12
|
+
super()
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def view_template
|
|
16
|
+
input(
|
|
17
|
+
type: "checkbox",
|
|
18
|
+
name: @name,
|
|
19
|
+
id: @id,
|
|
20
|
+
value: @value,
|
|
21
|
+
class: @options[:class] || "checkbox",
|
|
22
|
+
checked: @checked || nil
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
# Yielded for each item by Form#collection_check_boxes. Provides check_box and
|
|
5
|
+
# label builders mirroring Rails' collection_check_boxes API.
|
|
6
|
+
class CollectionCheckBoxBuilder
|
|
7
|
+
attr_reader :object, :value, :text
|
|
8
|
+
|
|
9
|
+
def initialize(object:, value:, text:, checked:, name:, id:)
|
|
10
|
+
@object = object
|
|
11
|
+
@value = value
|
|
12
|
+
@text = text
|
|
13
|
+
@checked = checked
|
|
14
|
+
@name = name
|
|
15
|
+
@id = id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check_box(options = {})
|
|
19
|
+
Forms::CollectionCheckBox.new(name: @name, id: @id, value: @value, checked: @checked, **options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def label(options = {}, &)
|
|
23
|
+
Forms::CollectionLabel.new(for_id: @id, text: @text, **options, &)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
# A label for a single collection checkbox item.
|
|
5
|
+
class CollectionLabel < Phlex::HTML
|
|
6
|
+
def initialize(for_id:, text:, **options, &block)
|
|
7
|
+
@for_id = for_id
|
|
8
|
+
@text = text
|
|
9
|
+
@options = options
|
|
10
|
+
@block = block
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def view_template
|
|
15
|
+
label(for: @for_id, class: @options[:class]) do
|
|
16
|
+
@block ? yield_content(&@block) : plain(@text)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
# Convenience: an Input pre-typed as email with a sensible default placeholder.
|
|
5
|
+
# EmailField(:primary, name: "user[email]")
|
|
6
|
+
class EmailField < Input
|
|
7
|
+
def initialize(*, placeholder: "email@example.com", **)
|
|
8
|
+
super(*, type: "email", placeholder:, **)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|