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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26a87406065dfe0b6d5b023949661d89548c3ad6379af9d0e4d74c4162f49070
4
- data.tar.gz: b36430b1faf88ad5095a358709bce31a72869c40619dc527bc3e04be10450c0f
3
+ metadata.gz: c9b781532660ae609037596969274d95acbb9eac449c60475c51a91329a9fcb5
4
+ data.tar.gz: e242d6ecb20a6eb2238b5a2dfb1bb86bd68ad5f2b7a783bdb9b23e2d6cc1c42c
5
5
  SHA512:
6
- metadata.gz: f9e5a95dfea504c6b487c23a0b5683a4a68391b4e07ff33192b548a40a5b7eff3a0562e0af1097a20bbeca3762017254fe626fb1fe7079408996815f8b1d7a71
7
- data.tar.gz: 1ae2b903bfe9057f1705b3d3a6e788c16acf1c4d837c0bdbd723434aca71c45432d194ebf638ce17c14d597826751a6c4bb124e92f02e7b3d01fdf29ac6bbf34
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
- Messages ship for `en` / `fr` / `af`; override via `window.PhlexForms.messages`.
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 : t("js.forms.validations.acceptance.accepted")
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 : t("js.forms.validations.acceptance.accepted")
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 : t("js.forms.validations.confirmation.confirmation")
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 ?? "")) ? t("js.forms.validations.exclusion.exclusion") : null
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 : t("js.forms.validations.format.invalid")
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 : t("js.forms.validations.inclusion.inclusion")
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 { t } from "phlex_forms/i18n"
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 t("js.forms.validations.length.wrong_length", { count: this.isValue })
48
+ return this.lengthMessage(this.wrongLengthMessageValue, "wrong_length", this.isValue)
45
49
  }
46
50
  if (this.hasMaximumValue && length > this.maximumValue) {
47
- return t("js.forms.validations.length.too_long", { count: this.maximumValue })
51
+ return this.lengthMessage(this.tooLongMessageValue, "too_long", this.maximumValue)
48
52
  }
49
53
  if (this.hasMinimumValue && length < this.minimumValue) {
50
- return t("js.forms.validations.length.too_short", { count: this.minimumValue })
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 t("js.forms.validations.numericality.not_a_number")
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 t("js.forms.validations.numericality.not_an_integer")
25
+ return this.messageOr("js.forms.validations.numericality.not_an_integer")
27
26
  }
28
27
  if (this.hasGreaterThanValue && !(number > this.greaterThanValue)) {
29
- return t("js.forms.validations.numericality.greater_than", { count: this.greaterThanValue })
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 t("js.forms.validations.numericality.greater_than_or_equal_to", {
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 t("js.forms.validations.numericality.less_than", { count: this.lessThanValue })
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 t("js.forms.validations.numericality.less_than_or_equal_to", {
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 t("js.forms.validations.numericality.equal_to", { count: this.equalToValue })
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 t("js.forms.validations.numericality.other_than", { count: this.otherThanValue })
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 t("js.forms.validations.numericality.odd")
56
- if (this.evenValue && truncated % 2 !== 0) return t("js.forms.validations.numericality.even")
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) ? t("js.forms.validations.presence.blank") : null
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).each do |key, value|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexForms
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
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.0
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