ember-changeset-validations
ember-changeset-validations copied to clipboard
Changing validation error messages at runtime
Hi, great addon.
I'm wondering, if there is a recommended way of changing validation messages at runtime?
I know it's possible to change it initially by defining a module that matches /validations\/messages$/gi
but it's only loaded once.
define(['foo/validations/messages'], () => {
return {default: {
...messages
}};
})
If a user changes its locale, it's impossible to update validation messages from what i can tell. Are there any known workarounds for supporting this kind of use-case?
@makepanic Are you using ember-i18n or equivalent?
If so, I could write a patch that tries to lookup the messages from an i18n service at run-time, that should account for a locale change.
Yes i'm using ember-intl
.
A configurable service sounds like a great idea.
I'm not a fan of the service idea in the addon itself, it would require a lot of rework since most of the addon are vanilla JS modules with no knowledge of Ember. Instead, you can use computed properties on an Ember.Object
:
const Messages = Ember.Object.extend({
intl: Ember.inject.service(),
inclusion: computed('intl', function() {
// ...
})
});
export default Messages.create();
That would work.
Thanks,
Using CPs inside the messages object works without problems.
(Except that i have to manually inject the intl
service using a container)
Another question:
Would it make sense to allow users to pass their own buildMessage
function to move string template interpolation away from the validation-errors
module to a pluggable part of the app (primarily i18n related libs)?
That way one isn't coupled to the string format ember-changeset-validations is using.
It could be solved by allowing the user to pass validatorOptions
to the lookupValidator
method: lookupValidator(Validator, {buildMessage: () => 'foo'})
which in turn will pass the validatorOptions into each validator.
Something like this: https://github.com/makepanic/ember-changeset-validations/commit/8cca573dfaebf71c0dec1368d66bdf7cf683eb18
@makepanic How did you get this to work? I can't seem to sort out the manual injection of the (I'm using ember-i18n
) service using container. Do you have a link to your implementation?
Here's what I ended up doing, in case anyone else ends up here looking for help: https://gist.github.com/jmacqueen/6d31aaa5ba5f5841d123d0406f1f86c5
@jmacqueen i went with passing a buildMessage
option to the lookupValidator
method (see https://github.com/makepanic/ember-changeset-validations/commit/8cca573dfaebf71c0dec1368d66bdf7cf683eb18).
Message building happens by calling the provided buildMessage
function which in turn calls ember-intl to translate the given incoming validation message key.
Hei @jmacqueen, i'm seeing this error in my console:
Uncaught TypeError: _siteUtilsApplication.default.instance.lookup is not a function
Any ideias why?
Thank you! <3
I was wondering if this addon could check whether message.js
export a POJO or an Ember.Object
. If it is an Ember object, it could take care of the creation and owner injection:
import Ember from 'ember';
const { computed } = Ember;
export default Ember.Object.extend({
i18n: Ember.inject.service(),
i18nName: computed('i18n.locale', function () {
return this.t('validations.i18nName');
})
});
// I do not create or export the object
And the addons would be responsible to:
import Messages from 'validations/messages.js';
Messages.create(
Ember.getOwner(this).ownerInjection()
);
Does it sound like it would work? And is it something you'd be willing to implement? cc @poteto @martndemus
@karellm I would be open to this. Do you have time to open a PR?
@poteto I tried but I'm not very familiar with Ember internals and add-ons development. this
is actually undefined
inside the utils
. It looks like it should be passed by buildMessage
which makes the API a little weird.
Also I'm not sure how to go about testing this since the resolver looks for validations/messages
so I can't have 2 different files (one POJO, one Ember.Object).
I'm happy to do it but I would really appreciate some pointers. Thanks!
@poteto, @karellm Any progress on this? I find myself stuck trying to get an Ember.Object to be recognized from messages.js. As long as it's a simple POJO - no issues, but when wrapped in an Ember.Object, the add-on reverts to its defaults rather than using the provided object from messages.js.
Hi all,
I have just forked this repo in order to implement this feature. @jmacqueen gave a good example on how to connect the Ember IoC with the messages.
Following his advice, I created an instance initializer that would pass the Application instance to the get-messages.js
file in order to lookup them up via Ember container. Using the container there is no need to iterate through all defined modules and addons in order to find the custom validations/messages.js
file.
You can check the fork here
To use this feature, you would simply need to create an instance-initializer
in your application and define it like so:
export {default} from 'ember-changeset-validations/instance-initializers/ember-changeset-validations';
Then in your validations/messages.js
file, you can define a property or implement the messageForKey
function to load a message from either i18n
, intl
or any Ember Service:
import Ember from 'ember';
const { get, computed } = Ember;
export default Ember.Object.extend({
i18n: Ember.inject.service(),
i18nName: computed('i18n.locale', function () {
return this.t('validations.i18nName');
}),
messageForKey(key) {
const i18n = this.get('i18n');
key = `validations.${key}`;
if (i18n.exists(key)) {
return i18n.t(key).toString();
}
}
});
If you are ok with this approach, I would gladly make a PR to incorporate these changes.
While working on this, I realised that that a simple API could be exposed in order to load custom messages and field descriptions. From the current issues it seems that there is a need for further customisation. It would be great if we could make this library be even more flexible with validation messages.
My suggestions would be to expose messageForKey
or descriptionForKey
functions. Message and description lookup would look like this:
Ember.Mixin.create({
// default messages
defaults: {},
getDescriptionFor(key = '') {
let value;
// Check if there is a custom lookup function defined
if (canInvoke(this, 'descriptionForKey')) {
value = this.descriptionForKey(key);
}
// Generate default description
if (isNone(value)) {
value = capitalize(dasherize(key).split(/[_-]/g).join(' '));
}
return value;
},
// use the unknown property hook in Ember.Object in order to find
// messages which were not defined directly on the object
unknownProperty(key = '') {
let value;
// Check if there is a custom lookup function defined
if (canInvoke(this, 'messageForKey')) {
value = this.messageForKey(key);
}
// Play nice with ObjectProxy instances
if (isNone(value)) {
value = this._super(key);
}
// Load default message
if (isNone(value)) {
value = get(this, `defaults.${key}`);
}
return value;
}
})
As this change would be a rather big one, I would be good to agree on this beforehand :)
Does the solution https://github.com/DockYard/ember-changeset-validations/issues/94#issuecomment-244089956 still work? It seems computed properties aren't merged by Ember.merge
in https://github.com/DockYard/ember-changeset-validations/blob/c638e81a537c5e9fb427227cc0e35852d01a6919/addon/utils/with-defaults.js#L13-L15
We implemented the changes @jmacqueen did, and the breakpoint on the initialize method in instance-initializers/application.js is hitting. However nothing is hitting for our validations/messages.js file. Is there something that needs to be added to our validators file to get our translated messages file loaded?
A sample of our validator file...
import {
validateLength,
validateFormat
} from 'ember-changeset-validations/validators';
import { validatePhone, validateExtension } from 'ember-engine-contacts/validators/phone';
import { regularExpressions } from 'ember-validators/format';
import validateTwitter from 'ember-engine-contacts/validators/twitter';
export default {
workEmail: [
validateLength({ max: 254, allowNone: true, allowBlank: true }),
validateFormat({ type: 'email', allowNone: true, allowBlank: true, regex: regularExpressions.email })
]
};
I also tried removing any code we had in both app/validations/messages and addon/validations/messages with
export default {
email: 'FOOBAR'
}
as the doc states to do, but still the default message showed.
I can add 'message' to the object parameters in the validateFormat method, which works...but not sure how to get our overridden messages file to work. Any help would be greatly appreciated!
@martndemus @poteto Could you suggest some solution for this issue, guys? I'm using ember-i18n service for my translations, and I've tried the option https://github.com/poteto/ember-changeset-validations/issues/94#issuecomment-244089956 , but this doesn't work anymore. Any other elegant suggestion? Thanks for your great work btw!
@makepanic @martndemus @karellm @nandorstanko I've released an Ember Addon to apply ember-i18n
translations to these messages in a beta version here https://github.com/mirai-audio/ember-i18n-changeset-validations. Could use a few hands to try it before I release a v1.0.0
Edit. I release v1.0.0
@0xadada Correct me if I'm wrong, but your solution doesn't support changing locale without reloading the app. That's a huge feature, that is a MUST HAVE for me... That is why I use ember-i18n addon in the first place. I've switched to ember-changeset-cp-validations+ember-i18n-cp-validations combo, and it's working for me. Probably slower this way, but not an issue so far.
@nandorstanko you are correct
While working on a new project with intl + changeset-validations, I've found another workaround (hack) to get this working without touching too many things:
- changeset-validations is retrieveing messages from ember-validators/messages
- if one overwrites
Messages.formatMessage
, one can provide a custom translation solution:- configure
validations/messages
to be a simple identity map where each key is its value. This causes formatMessage to be called with the error code (i.e.inclusion
).
- configure
export default {
inclusion: 'inclusion',
exclusion: 'exclusion',
...
url: 'url'
}
- change
Message.formatMessage
to take the error code "message" and context and pass it into intl:
Messages.formatMessage = (message, context = {}) => {
let errorKey = message;
if (isNone(errorKey) || typeof errorKey !== 'string') {
return intl.t('validations.invalid');
}
return intl.t(`validations.${errorKey}`, context);
}
Now if changeset-validations tries to format the translated template, it passes the error code and get's the translation using whatever you configured.
In ember-intl context one can easily add these error key translations.
validations:
inclusion: 'Ist nicht in der Liste enthalten'
...
I've added overwriting formatMessage
into the intl service init
hook and it looks like it's working fine.
Yesterday, I finally ran into the need to use a service in my validations. In my particular case, I needed to fetch a dynamic array of strings from my API to validate inclusion against. After reading this thread, I almost tried to make this work in this addon.
However, I realized that I would essentially be rebuilding a bunch of the stuff in ember-changeset-cp-validations. I decided to just start using that addon instead. I think that model is a lot more robust and fits better with any scenario where you need to access dynamic data provided by an Ember service. It was a pretty simple transition and I will probably move my other validations over if the need arises. Definitely recommended!
I've tried different approaches and addon to display/translate validations messages at runtime and finally I found an easy way to do it. I've followed https://github.com/poteto/ember-changeset-validations#overriding-validation-messages but instead of returning a real message I return a valid translation key for ember-i18n
.
// app/validations/messages.js
export default {
inclusion: 'errors.validations.inclusion',
exclusion: 'errors.validations.invalid',
invalid: 'errors.validations.invalid',
// ...
}
And now I my view I can use this key to translate validation message at runtime using the t
helper.
Like this:
{{#if changeset.isInvalid}}
<p>There were errors in your form:</p>
<ul>
{{#each changeset.errors as |error|}}
<li>{{error.key}}: {{t error.validation}}</li>
{{/each}}
</ul>
{{/if}}
what you can do is to register a seperate lookup util.
app/utils/lookup-validator.js
import originalLookupValidator from 'ember-changeset-validations';
import { merge } from '@ember/polyfills';
import { isArray } from '@ember/array';
export default function lookupValidator(validationMap = {}, options = {}) {
return ({ key, newValue, oldValue, changes, content }) => {
let validationResult = originalLookupValidator(validationMap)({ key, newValue, oldValue, changes, content });
if (options.intl && options.intl.t && isArray(validationResult)) {
validationResult = validationResult.map((v) => {
if (v.message && v.attrs) {
return options.intl.t(v.message, merge(v.attrs, { key: options.intl.t(`validations.changeset.keys.${key}`) }));
}
return v;
})
}
return validationResult;
}
}
app/validations/messages.js
export default {
inclusion: 'validations.changeset.messages.inclusion',
exclusion: 'validations.changeset.messages.exclusion',
invalid: 'validations.changeset.messages.invalid',
confirmation: "validations.changeset.messages.confirmation",
accepted: 'validations.changeset.messages.accepted',
empty: "validations.changeset.messages.empty",
blank: 'validations.changeset.messages.blank',
present: "validations.changeset.messages.present",
collection: 'validations.changeset.messages.collection',
singular: "validations.changeset.messages.singular",
tooLong: 'validations.changeset.messages.tooLong',
tooShort: 'validations.changeset.messages.tooShort',
between: 'validations.changeset.messages.between',
before: 'validations.changeset.messages.before',
onOrBefore: 'validations.changeset.messages.onOrBefore',
after: 'validations.changeset.messages.after',
onOrAfter: 'validations.changeset.messages.onOrAfter',
wrongDateFormat: 'validations.changeset.messages.wrongDateFormat',
wrongLength: 'validations.changeset.messages.wrongLength',
notANumber: 'validations.changeset.messages.notANumber',
notAnInteger: 'validations.changeset.messages.notAnInteger',
greaterThan: 'validations.changeset.messages.greaterThan',
greaterThanOrEqualTo: 'validations.changeset.messages.greaterThanOrEqualTo',
equalTo: 'validations.changeset.messages.equalTo',
lessThan: 'validations.changeset.messages.lessThan',
lessThanOrEqualTo: 'validations.changeset.messages.lessThanOrEqualTo',
otherThan: 'validations.changeset.messages.otherThan',
odd: 'validations.changeset.messages.odd',
even: 'validations.changeset.messages.even',
positive: 'validations.changeset.messages.positive',
multipleOf: 'validations.changeset.messages.multipleOf',
date: 'validations.changeset.messages.date',
email: 'validations.changeset.messages.email',
phone: 'validations.changeset.messages.phone',
url: 'validations.changeset.messages.url',
formatMessage: (message, attrs) => {
return { message, attrs };
}
}
/translations/de-de.yml
##############################
# VALIDATIONS
##############################
validations:
changeset:
messages:
inclusion: '{key} is not included in the list'
exclusion: '{key} is reserved'
invalid: '{key} is invalid'
confirmation: "{key} doesn't match {on}"
accepted: '{key} must be accepted'
empty: "{key} can't be empty"
blank: '{key} must be blank'
present: "{key} can't be blank"
collection: '{key} must be a collection'
singular: "{key} can't be a collection"
tooLong: '{key} is too long (maximum is {max} characters)'
tooShort: '{key} is too kurz (minimum is {min} characters)'
between: '{key} must be between {min} and {max} characters'
before: '{key} must be before {before}'
onOrBefore: '{key} must be on or before {onOrBefore}'
after: '{key} must be after {after}'
onOrAfter: '{key} must be on or after {onOrAfter}'
wrongDateFormat: '{key} must be in the format of {format}'
wrongLength: '{key} is the wrong length (should be {is} characters)'
notANumber: '{key} must be a number'
notAnInteger: '{key} must be an integer'
greaterThan: '{key} must be greater than {gt}'
greaterThanOrEqualTo: '{key} must be greater than or equal to {gte}'
equalTo: '{key} must be equal to {is}'
lessThan: '{key} must be less than {lt}'
lessThanOrEqualTo: '{key} must be less than or equal to {lte}'
otherThan: '{key} must be other than {value}'
odd: '{key} must be odd'
even: '{key} must be even'
positive: '{key} must be positive'
multipleOf: '{key} must be a multiple of {multipleOf}'
date: '{key} must be a valid date'
email: '{key} ist keine valide email'
phone: '{key} must be a valid phone number'
url: '{key} must be a valid url'
keys:
email: E-Mail
password: Passwort
and finally use it....
...
import lookupValidator from '../../../utils/lookup-validator';
...
return new Changeset({}, lookupValidator(UserValidations, {intl: this.intl}), UserValidations);
this way no other changes are required and it works out of the box with ember-form-for for example....
little hacky yes but this works as it should with every validator and its also possible to change locales at runtime. Hope this helps
Hi, I've come up with a PR that allows to postpone i18n/intl work outside of ember-changeset-validations by making it return (optionally) an object instead of a String. It works wonders for us: https://github.com/poteto/ember-changeset-validations/pull/185
I can confirm that @xcambar's solution is very flexible and works great for my use case.
FYI, #185 has been merged.
See the PR and this setion of the docs for info.
Should this issue be closed now?
Looks good. Can we maybe add an example on how to use this with intl/i18n?
I'm ok with closing this
I'm using it with something of the form (wrapped in a helper):
{{t (concat "path.to.errors." error.type) errors.context}}
I have kind of tackled this and our migration from ember-i18n
to ember-intl
at the same time. The one gotcha I ran into was that the {{t }}
helper from ember-i18n
allowed passing a hash of i18n paramaters, where the one from ember-intl
requires passing each as it's own parameter, which isn't ideal when we don't know the parameters upfront.
@xcambar passing errors.context
with name attribute is not working on my end ? Can you help with that ?
I can try :) In order to ease investigation, can you share a twiddle or a repo that reproduces the behaviour, please?
@xcambar I have setup it on https://github.com/gauravjain028/ember-intl-changeset-error-handling
Template File : https://github.com/gauravjain028/ember-intl-changeset-error-handling/blob/master/app/templates/application.hbs
When in load the app, it give me js error
The intl string context variable 'description' was not provided to the string '{description} can't be blank'
I was having issues in octane but I was able to get a good workaround with custom validators.
import Component from '@glimmer/component';
import Changeset from 'ember-changeset';
import lookupValidator from 'ember-changeset-validations';
import { inject as service } from '@ember/service';
export default class MyComponent extends Component {
@service intl;
validator({ errorKey }) {
let errorMsg = this.intl.t(`${errorKey}.error.msg`);
return function(_, value) {
let isInValid = ....; // custom validation logic
if (isInValid) {
return errorMsg;
}
return true;
}
}
validators = {
name: this.validator({ errorKey: 'name.field' }),
email: this.validator({ errorKey: 'email.field' })
};
changeset = new Changeset(
{
name: this.name,
email: this.email
},
lookupValidator(this.validators),
this.validators
);
}