phlex-forms 0.4.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +13 -1
- data/app/javascript/phlex_forms/controllers/validations/acceptance_controller.js +2 -3
- data/app/javascript/phlex_forms/controllers/validations/base_controller.js +12 -1
- data/app/javascript/phlex_forms/controllers/validations/confirmation_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/exclusion_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/format_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/inclusion_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/length_controller.js +15 -4
- data/app/javascript/phlex_forms/controllers/validations/numericality_controller.js +10 -11
- data/app/javascript/phlex_forms/controllers/validations/presence_controller.js +1 -2
- data/app/javascript/phlex_forms/i18n.js +1 -1
- data/lib/forms/validations/introspector.rb +24 -1
- data/lib/forms/validations/manual_rules.rb +3 -3
- data/lib/forms/validations/message.rb +77 -0
- data/lib/phlex_forms/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9b781532660ae609037596969274d95acbb9eac449c60475c51a91329a9fcb5
|
|
4
|
+
data.tar.gz: e242d6ecb20a6eb2238b5a2dfb1bb86bd68ad5f2b7a783bdb9b23e2d6cc1c42c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9bbc43663fb51f9e8f414702a4aaf7349870ca23f854ce3b9b38220dc32abe42f77820ab840a78708c473d5e30bbb9f918f6ae39025d94b5e94103e9b2ae3de
|
|
7
|
+
data.tar.gz: 9435b6699cab6da09d974077c8b2ff4f6f83d4f28b0cc44d2fd56419fea1b5bca2cb7347ef86b16a6ede417fddda62ce2d4c03c1e02d9e2ae58b315272701969
|
data/CHANGELOG.md
CHANGED
|
@@ -51,6 +51,18 @@ plus daisyUI's modifier groups). `PhlexForms::ClassMerge` is gone.
|
|
|
51
51
|
|
|
52
52
|
### Fixed
|
|
53
53
|
|
|
54
|
+
- **Client-side validators honour the validator's `message:`** (issue #41).
|
|
55
|
+
`validates :code, format: { with: /\A[a-z]*\z/, message: "lowercase only" }`
|
|
56
|
+
used to show the generic "is invalid" in the browser and the specific text
|
|
57
|
+
only after a round trip. The introspector now emits
|
|
58
|
+
`data-validations--<validator>-message-value`: a String as is (`%{count}`
|
|
59
|
+
stays for the browser to fill), a Symbol resolved on the server through
|
|
60
|
+
ActiveModel's lookup chain in the current locale, a Proc skipped (it needs a
|
|
61
|
+
record). Length's `too_long:` / `too_short:` / `wrong_length:` win over
|
|
62
|
+
`message:`, as on the server. Every shipped controller prefers the value and
|
|
63
|
+
falls back to its bundled message; the submit coordinator collects it too. A
|
|
64
|
+
`validate: { presence: { message: "…" } }` / `confirmation: { message: "…" }`
|
|
65
|
+
rules hash now keeps its options (it used to collapse them to `true`).
|
|
54
66
|
- **`rich_textarea` forwards its block into `<lexxy-editor>`**: Lexxy configures
|
|
55
67
|
an editor through CHILD elements — `<lexxy-prompt>` for @mentions,
|
|
56
68
|
`<lexxy-code-language-picker>` for code blocks — but the component rendered
|
data/README.md
CHANGED
|
@@ -345,7 +345,19 @@ the form-level `validations--form` coordinator), which `lazyLoadControllersFrom`
|
|
|
345
345
|
resolves to `phlex_forms/controllers/validations/*_controller` — the path the gem
|
|
346
346
|
ships them at.
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
A validator's own `message:` reaches the browser too, so the client shows what
|
|
349
|
+
the server would: a String as is (`%{value}` is filled in, and `%{count}` for
|
|
350
|
+
rules that have one: length and the numericality bounds), a Symbol resolved on
|
|
351
|
+
the server through your app's i18n, a Proc skipped (it needs a record).
|
|
352
|
+
Length's `too_long:` / `too_short:` / `wrong_length:` win over `message:`.
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
validates :code, format: { with: /\A[a-z]*\z/, message: "may only contain lowercase letters" }
|
|
356
|
+
f.field :note, validate: { length: { maximum: 30, too_long: "keep it under %{count}" } }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Without a `message:`, the bundled messages apply: they ship for `en` / `fr` /
|
|
360
|
+
`af`; override them page-wide via `window.PhlexForms.messages`.
|
|
349
361
|
|
|
350
362
|
## Nested attributes, collections & escape valves
|
|
351
363
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::AcceptanceValidator. For
|
|
5
4
|
// checkboxes, asserts `.checked`. For other inputs, asserts the
|
|
@@ -11,11 +10,11 @@ export default class extends FieldValidatorController {
|
|
|
11
10
|
|
|
12
11
|
check(value) {
|
|
13
12
|
if (this.element.type === "checkbox") {
|
|
14
|
-
return this.element.checked ? null :
|
|
13
|
+
return this.element.checked ? null : this.messageOr("js.forms.validations.acceptance.accepted")
|
|
15
14
|
}
|
|
16
15
|
try {
|
|
17
16
|
const list = JSON.parse(this.acceptValue)
|
|
18
|
-
return list.map(String).includes(String(value ?? "")) ? null :
|
|
17
|
+
return list.map(String).includes(String(value ?? "")) ? null : this.messageOr("js.forms.validations.acceptance.accepted")
|
|
19
18
|
} catch (_e) {
|
|
20
19
|
return null
|
|
21
20
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { interpolate, t } from "phlex_forms/i18n"
|
|
2
3
|
|
|
3
4
|
// Shared base for every field-level validator. Subclasses implement
|
|
4
5
|
// `check(value)` and return either `null` (valid) or a translation
|
|
@@ -8,7 +9,8 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
8
9
|
//
|
|
9
10
|
// Each subclass declares its own `static values` for the rules it
|
|
10
11
|
// reads from data attributes; the base class only knows about the
|
|
11
|
-
// `allowBlank` / `allowNil` short-circuits
|
|
12
|
+
// `allowBlank` / `allowNil` short-circuits and the `message` value,
|
|
13
|
+
// which carries the validator's own `message:` option (issue #41).
|
|
12
14
|
export class FieldValidatorController extends Controller {
|
|
13
15
|
// `error` is opt-in: callers that pre-render a `<p data-validations--<validator>-target="error">`
|
|
14
16
|
// get a stable slot the controller toggles. Inputs without an
|
|
@@ -18,6 +20,7 @@ export class FieldValidatorController extends Controller {
|
|
|
18
20
|
static values = {
|
|
19
21
|
allowBlank: { type: Boolean, default: false },
|
|
20
22
|
allowNil: { type: Boolean, default: false },
|
|
23
|
+
message: String,
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
// Element must be the input itself — these controllers are attached
|
|
@@ -64,6 +67,14 @@ export class FieldValidatorController extends Controller {
|
|
|
64
67
|
return null
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
// The validator's own `message:` when the server rendered one, else the
|
|
71
|
+
// bundled translation for `key`. Either way `%{count}` & co. are filled in;
|
|
72
|
+
// a custom message may also carry `%{value}`, as on the server.
|
|
73
|
+
messageOr(key, vars = {}) {
|
|
74
|
+
if (!(this.hasMessageValue && this.messageValue)) return t(key, vars)
|
|
75
|
+
return interpolate(this.messageValue, { value: this.fieldValue(), ...vars })
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
shouldSkip(value) {
|
|
68
79
|
if (this.allowNilValue && (value === null || value === undefined)) return true
|
|
69
80
|
if (this.allowBlankValue && this.isBlank(value)) return true
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::ConfirmationValidator. Looks
|
|
5
4
|
// for the `<name>_confirmation` field by name within the same form
|
|
@@ -15,7 +14,7 @@ export default class extends FieldValidatorController {
|
|
|
15
14
|
if (!form) return null
|
|
16
15
|
const partner = form.elements[this.matchValue] || this.partnerByAttribute(form)
|
|
17
16
|
if (!partner) return null
|
|
18
|
-
return String(partner.value) === String(value ?? "") ? null :
|
|
17
|
+
return String(partner.value) === String(value ?? "") ? null : this.messageOr("js.forms.validations.confirmation.confirmation")
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
partnerByAttribute(form) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::ExclusionValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -11,7 +10,7 @@ export default class extends FieldValidatorController {
|
|
|
11
10
|
if (!this.hasInValue) return null
|
|
12
11
|
try {
|
|
13
12
|
const list = JSON.parse(this.inValue)
|
|
14
|
-
return list.map(String).includes(String(value ?? "")) ?
|
|
13
|
+
return list.map(String).includes(String(value ?? "")) ? this.messageOr("js.forms.validations.exclusion.exclusion") : null
|
|
15
14
|
} catch (_e) {
|
|
16
15
|
return null
|
|
17
16
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::FormatValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -12,7 +11,7 @@ export default class extends FieldValidatorController {
|
|
|
12
11
|
if (!this.hasPatternValue) return null
|
|
13
12
|
try {
|
|
14
13
|
const regex = new RegExp(this.patternValue, this.flagsValue)
|
|
15
|
-
return regex.test(String(value ?? "")) ? null :
|
|
14
|
+
return regex.test(String(value ?? "")) ? null : this.messageOr("js.forms.validations.format.invalid")
|
|
16
15
|
} catch (_e) {
|
|
17
16
|
// If Rails' regex doesn't translate cleanly to JS, fall back to
|
|
18
17
|
// letting the server be the authority — don't block submit on a
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::InclusionValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -11,7 +10,7 @@ export default class extends FieldValidatorController {
|
|
|
11
10
|
if (!this.hasInValue) return null
|
|
12
11
|
try {
|
|
13
12
|
const list = JSON.parse(this.inValue)
|
|
14
|
-
return list.map(String).includes(String(value ?? "")) ? null :
|
|
13
|
+
return list.map(String).includes(String(value ?? "")) ? null : this.messageOr("js.forms.validations.inclusion.inclusion")
|
|
15
14
|
} catch (_e) {
|
|
16
15
|
return null
|
|
17
16
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import {
|
|
2
|
+
import { interpolate } from "phlex_forms/i18n"
|
|
3
3
|
|
|
4
4
|
// Mirrors ActiveModel::Validations::LengthValidator. Reads:
|
|
5
5
|
// - maximum / minimum / is
|
|
6
|
+
// - too-long / too-short / wrong-length messages (win over `message`)
|
|
6
7
|
// - allow-blank / allow-nil
|
|
7
8
|
//
|
|
8
9
|
// Counter is rendered next to the input when `maximum` is set, so
|
|
@@ -20,6 +21,9 @@ export default class extends FieldValidatorController {
|
|
|
20
21
|
maximum: Number,
|
|
21
22
|
minimum: Number,
|
|
22
23
|
is: Number,
|
|
24
|
+
tooLongMessage: String,
|
|
25
|
+
tooShortMessage: String,
|
|
26
|
+
wrongLengthMessage: String,
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
connect() {
|
|
@@ -41,17 +45,24 @@ export default class extends FieldValidatorController {
|
|
|
41
45
|
const length = this.codepointLength(value)
|
|
42
46
|
|
|
43
47
|
if (this.hasIsValue && length !== this.isValue) {
|
|
44
|
-
return
|
|
48
|
+
return this.lengthMessage(this.wrongLengthMessageValue, "wrong_length", this.isValue)
|
|
45
49
|
}
|
|
46
50
|
if (this.hasMaximumValue && length > this.maximumValue) {
|
|
47
|
-
return
|
|
51
|
+
return this.lengthMessage(this.tooLongMessageValue, "too_long", this.maximumValue)
|
|
48
52
|
}
|
|
49
53
|
if (this.hasMinimumValue && length < this.minimumValue) {
|
|
50
|
-
return
|
|
54
|
+
return this.lengthMessage(this.tooShortMessageValue, "too_short", this.minimumValue)
|
|
51
55
|
}
|
|
52
56
|
return null
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// A per-case message (`too_long:` & co.) wins over `message:`, as on the
|
|
60
|
+
// server; the bundled translation is the final fallback.
|
|
61
|
+
lengthMessage(caseMessage, key, count) {
|
|
62
|
+
if (caseMessage) return interpolate(caseMessage, { value: this.fieldValue(), count })
|
|
63
|
+
return this.messageOr(`js.forms.validations.length.${key}`, { count })
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
updateCounter = () => {
|
|
56
67
|
const counter = this.counterElement()
|
|
57
68
|
if (!counter) return
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::NumericalityValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -20,40 +19,40 @@ export default class extends FieldValidatorController {
|
|
|
20
19
|
if (raw === "") return null
|
|
21
20
|
|
|
22
21
|
const number = Number(raw)
|
|
23
|
-
if (Number.isNaN(number)) return
|
|
22
|
+
if (Number.isNaN(number)) return this.messageOr("js.forms.validations.numericality.not_a_number")
|
|
24
23
|
|
|
25
24
|
if (this.onlyIntegerValue && !Number.isInteger(number)) {
|
|
26
|
-
return
|
|
25
|
+
return this.messageOr("js.forms.validations.numericality.not_an_integer")
|
|
27
26
|
}
|
|
28
27
|
if (this.hasGreaterThanValue && !(number > this.greaterThanValue)) {
|
|
29
|
-
return
|
|
28
|
+
return this.messageOr("js.forms.validations.numericality.greater_than", { count: this.greaterThanValue })
|
|
30
29
|
}
|
|
31
30
|
if (this.hasGreaterThanOrEqualToValue && !(number >= this.greaterThanOrEqualToValue)) {
|
|
32
|
-
return
|
|
31
|
+
return this.messageOr("js.forms.validations.numericality.greater_than_or_equal_to", {
|
|
33
32
|
count: this.greaterThanOrEqualToValue,
|
|
34
33
|
})
|
|
35
34
|
}
|
|
36
35
|
if (this.hasLessThanValue && !(number < this.lessThanValue)) {
|
|
37
|
-
return
|
|
36
|
+
return this.messageOr("js.forms.validations.numericality.less_than", { count: this.lessThanValue })
|
|
38
37
|
}
|
|
39
38
|
if (this.hasLessThanOrEqualToValue && !(number <= this.lessThanOrEqualToValue)) {
|
|
40
|
-
return
|
|
39
|
+
return this.messageOr("js.forms.validations.numericality.less_than_or_equal_to", {
|
|
41
40
|
count: this.lessThanOrEqualToValue,
|
|
42
41
|
})
|
|
43
42
|
}
|
|
44
43
|
if (this.hasEqualToValue && number !== this.equalToValue) {
|
|
45
|
-
return
|
|
44
|
+
return this.messageOr("js.forms.validations.numericality.equal_to", { count: this.equalToValue })
|
|
46
45
|
}
|
|
47
46
|
if (this.hasOtherThanValue && number === this.otherThanValue) {
|
|
48
|
-
return
|
|
47
|
+
return this.messageOr("js.forms.validations.numericality.other_than", { count: this.otherThanValue })
|
|
49
48
|
}
|
|
50
49
|
// Rails' NumericalityValidator coerces with `.to_i` before
|
|
51
50
|
// checking parity, so `2.5` is treated as `2` (even). JS `%` on
|
|
52
51
|
// a float diverges from that — truncate first to keep client and
|
|
53
52
|
// server in agreement.
|
|
54
53
|
const truncated = Math.trunc(number)
|
|
55
|
-
if (this.oddValue && truncated % 2 === 0) return
|
|
56
|
-
if (this.evenValue && truncated % 2 !== 0) return
|
|
54
|
+
if (this.oddValue && truncated % 2 === 0) return this.messageOr("js.forms.validations.numericality.odd")
|
|
55
|
+
if (this.evenValue && truncated % 2 !== 0) return this.messageOr("js.forms.validations.numericality.even")
|
|
57
56
|
return null
|
|
58
57
|
}
|
|
59
58
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::PresenceValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -9,6 +8,6 @@ export default class extends FieldValidatorController {
|
|
|
9
8
|
|
|
10
9
|
check(value) {
|
|
11
10
|
if (!this.requiredValue) return null
|
|
12
|
-
return this.isBlank(value) ?
|
|
11
|
+
return this.isBlank(value) ? this.messageOr("js.forms.validations.presence.blank") : null
|
|
13
12
|
}
|
|
14
13
|
}
|
|
@@ -26,7 +26,7 @@ function lookup(scope, locale) {
|
|
|
26
26
|
return scope.split(".").reduce((node, key) => (node == null ? undefined : node[key]), table)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function interpolate(string, vars) {
|
|
29
|
+
export function interpolate(string, vars = {}) {
|
|
30
30
|
return string.replace(/%\{(\w+)\}/g, (_, key) => (key in vars ? String(vars[key]) : `%{${key}}`))
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -46,6 +46,11 @@ module Forms
|
|
|
46
46
|
only_integer odd even
|
|
47
47
|
].freeze
|
|
48
48
|
|
|
49
|
+
# LengthValidator's per-case messages; each wins over `message:` on the
|
|
50
|
+
# server, so the length controller checks them first. Each maps to the
|
|
51
|
+
# bound it reports, so a Symbol resolves with that count.
|
|
52
|
+
LENGTH_MESSAGE_KEYS = { too_long: :maximum, too_short: :minimum, wrong_length: :is }.freeze
|
|
53
|
+
|
|
49
54
|
# Convenience: returns a null introspector for nil models so
|
|
50
55
|
# callers don't need to guard.
|
|
51
56
|
def self.for(model_or_class)
|
|
@@ -83,7 +88,8 @@ module Forms
|
|
|
83
88
|
next unless suffix
|
|
84
89
|
|
|
85
90
|
controllers << "#{CONTROLLER_PREFIX}--#{suffix}"
|
|
86
|
-
value_attributes(suffix, validator, attribute).
|
|
91
|
+
value_attributes(suffix, validator, attribute).merge(message_attrs(suffix, validator, attribute))
|
|
92
|
+
.each do |key, value|
|
|
87
93
|
attrs[data_key(suffix, key)] = value
|
|
88
94
|
end
|
|
89
95
|
end
|
|
@@ -131,6 +137,23 @@ module Forms
|
|
|
131
137
|
end
|
|
132
138
|
end
|
|
133
139
|
|
|
140
|
+
# `message` (and length's `too_long_message` & co.) values, for the
|
|
141
|
+
# validator's own `message:` options. Omitted when there is none, or when
|
|
142
|
+
# it can't be resolved without a record, so the generic message stays.
|
|
143
|
+
def message_attrs(suffix, validator, attribute)
|
|
144
|
+
counts = suffix == "length" ? length_message_counts(validator) : {}
|
|
145
|
+
options = counts.keys + [:message]
|
|
146
|
+
options.each_with_object({}) do |option, attrs|
|
|
147
|
+
text = Message.resolve(validator.options[option], model_class:, attribute:, count: counts[option])
|
|
148
|
+
attrs[option == :message ? :message : :"#{option}_message"] = text if text
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def length_message_counts(validator)
|
|
153
|
+
bounds = length_attrs(validator)
|
|
154
|
+
LENGTH_MESSAGE_KEYS.to_h { |option, bound| [option, bounds[bound]&.to_i] }
|
|
155
|
+
end
|
|
156
|
+
|
|
134
157
|
def presence_attrs
|
|
135
158
|
{ required: "true" }
|
|
136
159
|
end
|
|
@@ -13,7 +13,7 @@ module Forms
|
|
|
13
13
|
# ManualRules.new(
|
|
14
14
|
# length: { maximum: 60, minimum: 3 },
|
|
15
15
|
# presence: true,
|
|
16
|
-
# format: { with: /\A[a-z]+\z/i },
|
|
16
|
+
# format: { with: /\A[a-z]+\z/i, message: "letters only" },
|
|
17
17
|
# numericality: { greater_than_or_equal_to: 0 },
|
|
18
18
|
# inclusion: { in: %w[a b c] },
|
|
19
19
|
# exclusion: { in: %w[admin] },
|
|
@@ -49,7 +49,7 @@ module Forms
|
|
|
49
49
|
captured_rules.each do |key, opts|
|
|
50
50
|
case key
|
|
51
51
|
when :presence
|
|
52
|
-
validates :value, presence: true if opts
|
|
52
|
+
validates :value, presence: opts.is_a?(Hash) ? opts : true if opts
|
|
53
53
|
when :length
|
|
54
54
|
validates :value, length: opts
|
|
55
55
|
when :format
|
|
@@ -61,7 +61,7 @@ module Forms
|
|
|
61
61
|
when :exclusion
|
|
62
62
|
validates :value, exclusion: opts
|
|
63
63
|
when :confirmation
|
|
64
|
-
validates :value, confirmation: true if opts
|
|
64
|
+
validates :value, confirmation: opts.is_a?(Hash) ? opts : true if opts
|
|
65
65
|
when :acceptance
|
|
66
66
|
validates :value, acceptance: opts.is_a?(Hash) ? opts : { accept: %w[1 true] }
|
|
67
67
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
module Validations
|
|
5
|
+
# Turns a validator's `message:` option into the text the browser should
|
|
6
|
+
# show, so the client mirror says what the server would (issue #41).
|
|
7
|
+
#
|
|
8
|
+
# - a String is emitted as is; `%{count}`-style placeholders stay in place
|
|
9
|
+
# for the Stimulus controller to fill, as it does for its own messages.
|
|
10
|
+
# A blank String resolves to nil: the browser reads "" as valid.
|
|
11
|
+
# - a Symbol is resolved on the server through ActiveModel's lookup chain
|
|
12
|
+
# (`<scope>.errors.models.<model>.attributes.<attr>.<key>` ... then
|
|
13
|
+
# `errors.messages.<key>`), in the current locale. With a known `count:`
|
|
14
|
+
# (length's per-case messages) it is passed to I18n, so a pluralised
|
|
15
|
+
# translation picks the right form; otherwise `%{count}` stays a
|
|
16
|
+
# placeholder and the plural `:other` form is used.
|
|
17
|
+
# - anything else (a Proc needs a record) resolves to nil, so the
|
|
18
|
+
# controller keeps its generic message.
|
|
19
|
+
#
|
|
20
|
+
# Mirrors `ActiveModel::Error.generate_message` against the class alone: the
|
|
21
|
+
# introspector never instantiates the model.
|
|
22
|
+
class Message
|
|
23
|
+
# Interpolations the browser fills in, kept as literal placeholders.
|
|
24
|
+
PLACEHOLDERS = { count: "%{count}", value: "%{value}" }.freeze
|
|
25
|
+
|
|
26
|
+
def self.resolve(message, model_class:, attribute:, count: nil)
|
|
27
|
+
case message
|
|
28
|
+
when String then message unless message.empty?
|
|
29
|
+
when Symbol then new(model_class, attribute).translate(message, count:)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(model_class, attribute)
|
|
34
|
+
@model_class = model_class
|
|
35
|
+
@attribute = attribute.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def translate(key, count: nil)
|
|
39
|
+
return unless defined?(I18n)
|
|
40
|
+
|
|
41
|
+
first, *rest = lookup_keys(key)
|
|
42
|
+
vars = interpolations
|
|
43
|
+
vars[:count] = count unless count.nil?
|
|
44
|
+
I18n.translate(first, **vars, default: rest, raise: true)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :model_class, :attribute
|
|
52
|
+
|
|
53
|
+
def lookup_keys(key)
|
|
54
|
+
model_keys(key) + [:"errors.attributes.#{attribute}.#{key}", :"errors.messages.#{key}"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def model_keys(key)
|
|
58
|
+
return [] unless model_class.respond_to?(:i18n_scope) && model_class.respond_to?(:lookup_ancestors)
|
|
59
|
+
|
|
60
|
+
scope = model_class.i18n_scope
|
|
61
|
+
keys = model_class.lookup_ancestors.flat_map do |klass|
|
|
62
|
+
i18n_key = klass.model_name.i18n_key
|
|
63
|
+
[:"#{scope}.errors.models.#{i18n_key}.attributes.#{attribute}.#{key}",
|
|
64
|
+
:"#{scope}.errors.models.#{i18n_key}.#{key}"]
|
|
65
|
+
end
|
|
66
|
+
keys << :"#{scope}.errors.messages.#{key}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def interpolations
|
|
70
|
+
names = {}
|
|
71
|
+
names[:model] = model_class.model_name.human if model_class.respond_to?(:model_name)
|
|
72
|
+
names[:attribute] = model_class.human_attribute_name(attribute) if model_class.respond_to?(:human_attribute_name)
|
|
73
|
+
PLACEHOLDERS.merge(names)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/phlex_forms/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex-forms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -178,6 +178,7 @@ files:
|
|
|
178
178
|
- lib/forms/toggle.rb
|
|
179
179
|
- lib/forms/validations/introspector.rb
|
|
180
180
|
- lib/forms/validations/manual_rules.rb
|
|
181
|
+
- lib/forms/validations/message.rb
|
|
181
182
|
- lib/forms/wrapped_input.rb
|
|
182
183
|
- lib/phlex-forms.rb
|
|
183
184
|
- lib/phlex_forms.rb
|