Skip to content

Commit

Permalink
feat: add lazy validators initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismllr committed Jun 9, 2021
1 parent 58a144e commit c202086
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try

# Until TS Linting is set up
/index.d.ts
73 changes: 61 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,48 @@ class MyForm extends Component {
}
```

### ValidationState type definition
```hbs
<input value={{this.username}} />
{{#unless this.formValidState.attrs.username.isValid}}
{{#each this.formValidState.attrs.username.messages as |msg|}}
<p>{{msg}}</p>
{{/each}}
{{/unless}}
```

```ts
interface AttributeValidation {
messages: string[];
isValid: boolean;
}
You can also pass a "thunk" to the `validationState` decorator, for lazy initialization of your Validators:

interface Attrs {
[propertyName: string]: AttributeValidation;
}
```js
import Component from '@glimmer/component';
import validationState, { validate } from 'ember-validation-state';

const Validators = {
username: [validate('presence', { presence: true })],
password: [validate('length', { min: 6 })]
};

class MyForm extends Component {
@tracked username = null;
@tracked password = null;

@validationState((component) => component.limitedValidators) validationState;

interface ValidationState {
isValid: boolean;
attrs: Attrs
get limitedValidators() {
if (!this.args.username) {
return {
password: Validators['password']
};
}
}
}
```

### ValidationState definition

Please refer to the types in [`index.d.ts`](./index.d.ts) for full typescript type definitions.


## 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.
Expand Down Expand Up @@ -160,6 +184,31 @@ class MyForm extends Component {
}
```

## Usage with Typescript

This package, although not yet rewritten in Typescript, is fully compatible and exports its own types.<br>

To have full typings support of the property initialized by `validationState`, utilize the `ValidationState` type:

```ts
import validationState, {
validate,
ValidationState,
} from 'ember-validation-state';

const AttrValidators = {
name: [validate('presence', { presence: true })],
description: [validate('presence', { presence: true })],
};

export default class {
@validationState(AttrValidators)
declare formValidState: ValidationState<typeof AttrValidators>;
}
```

Utilizing the generic argument `typeof AttrValidators` provides autocomplete for the `formValidState.attrs` hash.

## Contributing

See the [Contributing](CONTRIBUTING.md) guide for details.
Expand Down
11 changes: 9 additions & 2 deletions addon/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getOwner } from '@ember/application';
import { typeOf } from '@ember/utils';
import macro from 'macro-decorators';
import { validate as _validate } from 'ember-validators';

Expand Down Expand Up @@ -34,19 +35,25 @@ import { validate as _validate } from 'ember-validators';
*/
export default function validationState(VALIDATOR_FNS) {
return macro(function () {
let validatorFns = VALIDATOR_FNS;

if (typeOf(VALIDATOR_FNS) === 'function') {
validatorFns = VALIDATOR_FNS(this);
}

const attrState = {};

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

let formValid = true;

for (const key in VALIDATOR_FNS) {
for (const key in validatorFns) {
attrState[key] = {
messages: [],
isValid: true
};

for (const validator of VALIDATOR_FNS[key]) {
for (const validator of validatorFns[key]) {
const [computedValid, message] = validator.apply(this, [
this[key],
messages
Expand Down
21 changes: 21 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type Validators = Record<string, any[]>;

type AttributeValidation = {
messages: string[];
isValid: boolean;
};

type AttrsType<T> = {
[P in keyof T]: AttributeValidation;
};

export type ValidationState<A> = {
isValid: boolean;
attrs: AttrsType<A>;
};

export function validate(any, any): any;

export default function validationState(
validators: Validators | ((ctx: any) => Validators)
): PropertyDescriptor<ValidationState<typeof validators>>;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import validationState, { validate } from 'ember-validation-state';

module('Integration | Decorator | formValidation', function (hooks) {
module('Integration | Decorator | validationState', function (hooks) {
setupApplicationTest(hooks);

test('returns a state object, which has a falsey `isValid` value if one key fails any of its validations', function (assert) {
Expand All @@ -26,6 +26,28 @@ module('Integration | Decorator | formValidation', function (hooks) {
assert.equal(svc.validState.isValid, false);
});

test('can pass a lazy "thunk" for validator initialization', function (assert) {
class ValidatedService extends Service {
email = null;

@validationState((component) => component.validations) validState;

get validations() {
return {
email: [
validate('presence', { presence: true }),
validate('format', { type: 'email' })
]
};
}
}

this.owner.register('service:validated', ValidatedService);
const svc = this.owner.lookup('service:validated');

assert.equal(svc.validState.isValid, false);
});

test('invalid key returns a message to display', function (assert) {
const validations = {
email: [
Expand Down

0 comments on commit c202086

Please sign in to comment.