Skip to content

Commit

Permalink
complete addon with intl messages support
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismllr committed Jun 15, 2020
1 parent c22bc16 commit 4f6d03b
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 12 deletions.
105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ember install ember-validation-state
## Usage

```js
import Component from '@glimmer/component';
import validationState, { validate } from 'ember-validation-state';

const Validators = {
Expand Down Expand Up @@ -55,6 +56,110 @@ interface ValidationState {
}
```

### Intl

By default, if ember-intl is installed, validationState will attempt to look for a message for a specific validation error in your translations file. If no key is present, it will fall back to the ember-validators message.

```yaml
# en-us.yml

errors:
# provide intl version of of ember-validators `blank`
blank: '{description} cannot be blank'
```
#### `intlKey`
Pass `intlKey` if you would like to use a different intl key. Will be prefixed with `errors.` for the translations file lookup

```yaml
# en-us.yml
errors:
username-empty: 'Gotta fill in username'
```

```js
import { validate } from 'ember-validation-state';
const Validators = {
username: [validate('presence', { presence: true, intlKey: 'username-empty' })]
};
```

#### `descriptionKey`
Pass `descriptionKey` if you would like to internationalize the "description" of the field. Default is "This field". Will be prefixed with `errors.` for the translations file lookup

```yaml
# en-us.yml
errors:
usernames: 'Username'
# message that `descriptionKey` lookup will be inserted into
blank: '{description} cannot be blank'
```
```js
import { validate } from 'ember-validation-state';

const Validators = {
username: [validate('presence', { presence: true, descriptionKey: 'usernames' })]
};
```


### Custom validation methods

Custom validation methods can be passed in the array for a specific key. They are passed along the Messages builder for convenience.

**Message builder definition**
```ts
interface MessagesSvc {
getMessageFor(type: string, context: Object): string
}
```

**Validator signature**
```ts
function Validator(value: T, MessagesSvc): [isValid: boolean, message: string]
```

In action:

```yaml
# en-us.yml
errors:
password-regex: 'Password does not match required format'
```

```js
import Component from '@glimmer/component';
import validationState, { validate } from 'ember-validation-state';
function passwordRegex(value, messagesSvc) {
return [
/W/.test(value),
messagesSvc.getMessageFor('password-regex')
];
}
const Validators = {
username: [validate('presence', { presence: true })],
password: [
validate('length', { min: 6 }),
passwordRegex
]
};
class MyForm extends Component {
@tracked username = null;
@tracked password = null;
@validationState(Validators) validationState;
}
```

## Contributing

See the [Contributing](CONTRIBUTING.md) guide for details.
Expand Down
13 changes: 13 additions & 0 deletions addon/-private/di-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Inspiration: https://github.com/NullVoxPopuli/ember-contextual-services/blob/master/addon/contextual-service.ts

// To integrate with Ember's D.I. system, this is all that's needed.
// This is provided as a convience class for creating injectable classes
export default class DIProvider {
static create(injections) {
return new this(injections);
}

constructor(injections) {
Object.assign(this, injections);
}
}
20 changes: 11 additions & 9 deletions addon/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getOwner } from '@ember/application';
import macro from 'macro-decorators';
import { validate as _validate } from 'ember-validators';
import EVMessages from 'ember-validators/messages';

/**
* provides a macro decorator for form field validation state.
Expand Down Expand Up @@ -35,6 +35,10 @@ import EVMessages from 'ember-validators/messages';
export default function validationState(VALIDATOR_FNS) {
return macro(function () {
const attrState = {};

const messages = getOwner(this)
.lookup('validation-state:messages');

let formValid = true;

for (const key in VALIDATOR_FNS) {
Expand All @@ -44,7 +48,7 @@ export default function validationState(VALIDATOR_FNS) {
};

for (const validator of VALIDATOR_FNS[key]) {
const [computedValid, message] = validator(this[key]);
const [computedValid, message] = validator.apply(this, [this[key], messages]);

if (computedValid) {
continue;
Expand Down Expand Up @@ -76,20 +80,18 @@ export default function validationState(VALIDATOR_FNS) {
* };
*
* @param {string} eventName - the name of the validator to use
* @param {Object} context - the options hash passed along to ember-validators
* @returns {Function<[isValid: boolean, message?: string]>}
* @param {ibject} context - the options hash passed along to ember-validators
* @returns {Function<*>: [isValid: boolean, message?: string]}
*/
export function validate(eventName, context) {
return function (value) {
return function (value, messageSvc) {
const validOrContext = _validate(eventName, value, context);

if (typeof validOrContext === 'boolean') {
return [true];
}

const message = EVMessages.getMessageFor(validOrContext.type, {
description: 'This field',
...context
});
const message = messageSvc.getMessageFor(validOrContext.type, context);

return [false, message];
};
Expand Down
9 changes: 9 additions & 0 deletions addon/initializers/validation-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Messages from '../messages';

export function initialize(application) {
application.register('validation-state:messages', Messages);
}

export default {
initialize
};
55 changes: 55 additions & 0 deletions addon/validators/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import EmberObject from '@ember/object';
import { inject as service } from '@ember/service';
import { warn } from '@ember/debug';
import ValidatorsMessages from 'ember-validators/messages';
import DIProvider from '../-private/di-provider';

export default class ValidationMessages extends DIProvider {
@service intl;

prefix = 'errors';

constructor() {
super(...arguments);

if (!this.intl) {
warn(
`[validation-state] ember-intl not present. Falling back to default ember-validators Message builder`,
{ id: 'validation-state.messages.no-intl' }
);
}
}

getDescription(context) {
if (this.intl) {
return this.intl.exists(`${this.prefix}${context.descriptionKey}`) &&
this.intl.t(`${this.prefix}${context.descriptionKey}`);
}

return context.description || ValidatorsMessages.defaultDescription;
}

getMessageFor(type, context) {
const { prefix } = this;
const intlKey = context.intlKey || type;

context.description = this.getDescription(context);

// if ember-intl addon is not installed, call the validators method
if (!this.intl) {
return ValidatorsMessages.getMessageFor(...arguments);
}

// Otherwise, look it up in intl
if (this.intl.exists(key)) {
return this.intl.t(`${prefix}.${intlKey}`, context);
}

warn(
`[validation-state] Internationalized string not provided for <${type}>. Falling back to ember-validators Message builder`,
{ id: 'validation-state.messages.not-defined-intl' }
);

return ValidatorsMessages.getMessageFor(...arguments);
}
}
1 change: 1 addition & 0 deletions app/initializers/validation-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, initialize } from 'ember-validation-state/initializers/validation-state';
1 change: 1 addition & 0 deletions app/validators/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-validation-state/validators/messages';
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "ember-validation-state",
"version": "0.0.0",
"version": "0.0.1",
"description": "The default blueprint for ember-cli addons.",
"keywords": [
"ember-addon"
],
"repository": "",
"repository": "https://github.com/chrismllr/ember-validation-state",
"license": "MIT",
"author": "",
"author": "Chris Miller <[email protected]>",
"directories": {
"doc": "doc",
"test": "tests"
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/initializers/validation-state-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Application from '@ember/application';

import { initialize } from 'dummy/initializers/validation-state';
import { module, test } from 'qunit';
import { run } from '@ember/runloop';

module('Unit | Initializer | validation-state', function(hooks) {
hooks.beforeEach(function() {
this.TestApplication = Application.extend();
this.TestApplication.initializer({
name: 'initializer under test',
initialize
});

this.application = this.TestApplication.create({ autoboot: false });
});

hooks.afterEach(function() {
run(this.application, 'destroy');
});

// Replace this with your real tests.
test('it works', async function(assert) {
await this.application.boot();

assert.ok(true);
});
});

0 comments on commit 4f6d03b

Please sign in to comment.