Merge pull request #362 from ing-bank/feat/newValidation

New Validation System
This commit is contained in:
Thomas Allmer 2019-11-18 15:33:56 +01:00 committed by GitHub
commit 9a96c3d7b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
145 changed files with 6223 additions and 4647 deletions

View file

@ -21,6 +21,8 @@ npm i --save @lion/checkbox @lion/checkbox-group
```js ```js
import '@lion/checkbox/lion-checkbox.js'; import '@lion/checkbox/lion-checkbox.js';
import '@lion/checkbox-group/lion-checkbox-group.js'; import '@lion/checkbox-group/lion-checkbox-group.js';
// validator import example
import { Required } from '@lion/validate';
``` ```
### Example ### Example
@ -29,8 +31,8 @@ import '@lion/checkbox-group/lion-checkbox-group.js';
<lion-form><form> <lion-form><form>
<lion-checkbox-group <lion-checkbox-group
name="scientistsGroup" name="scientistsGroup"
label="Who are your favorite scientists?" label="Favorite scientists"
.errorValidators=${[['required']]} .validators=${[new Required()]}
> >
<lion-checkbox name="scientists[]" label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox> <lion-checkbox name="scientists[]" label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
<lion-checkbox name="scientists[]" label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox> <lion-checkbox name="scientists[]" label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>

View file

@ -37,8 +37,8 @@
}, },
"devDependencies": { "devDependencies": {
"@lion/checkbox": "^0.2.1", "@lion/checkbox": "^0.2.1",
"@lion/form": "^0.2.1",
"@lion/localize": "^0.5.0", "@lion/localize": "^0.5.0",
"@lion/validate": "^0.3.1",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4", "@open-wc/testing": "^2.3.4",
"sinon": "^7.2.2" "sinon": "^7.2.2"

View file

@ -2,20 +2,16 @@ import { LionFieldset } from '@lion/fieldset';
export class LionCheckboxGroup extends LionFieldset { export class LionCheckboxGroup extends LionFieldset {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
__isRequired(modelValues) { _isEmpty(modelValues) {
const keys = Object.keys(modelValues); const keys = Object.keys(modelValues);
for (let i = 0; i < keys.length; i += 1) { for (let i = 0; i < keys.length; i += 1) {
const modelValue = modelValues[keys[i]]; const modelValue = modelValues[keys[i]];
if (Array.isArray(modelValue)) { if (Array.isArray(modelValue)) {
// grouped via myName[] // grouped via myName[]
return { return !modelValue.some(node => node.checked);
required: modelValue.some(node => node.checked),
};
} }
return { return !modelValue.checked;
required: modelValue.checked,
};
} }
return { required: false }; return true;
} }
} }

View file

@ -2,180 +2,156 @@ import { storiesOf, html } from '@open-wc/demoing-storybook';
import '../lion-checkbox-group.js'; import '../lion-checkbox-group.js';
import '@lion/checkbox/lion-checkbox.js'; import '@lion/checkbox/lion-checkbox.js';
import '@lion/form/lion-form.js'; import { Required, Validator } from '@lion/validate';
import { localize } from '@lion/localize';
storiesOf('Forms|Checkbox Group', module) storiesOf('Forms|Checkbox Group', module)
.add( .add(
'Default', 'Default',
() => html` () => html`
<lion-form> <lion-checkbox-group name="scientistsGroup" label="Favorite scientists">
<form> <lion-checkbox
<lion-checkbox-group name="scientistsGroup" label="Who are your favorite scientists?"> name="scientists[]"
<lion-checkbox label="Archimedes"
name="scientists[]" .choiceValue=${'Archimedes'}
label="Archimedes" ></lion-checkbox>
.choiceValue=${'Archimedes'} <lion-checkbox
></lion-checkbox> name="scientists[]"
<lion-checkbox label="Francis Bacon"
name="scientists[]" .choiceValue=${'Francis Bacon'}
label="Francis Bacon" ></lion-checkbox>
.choiceValue=${'Francis Bacon'} <lion-checkbox
></lion-checkbox> name="scientists[]"
<lion-checkbox label="Marie Curie"
name="scientists[]" .choiceValue=${'Marie Curie'}
label="Marie Curie" ></lion-checkbox>
.choiceValue=${'Marie Curie'} </lion-checkbox-group>
></lion-checkbox>
</lion-checkbox-group>
</form>
</lion-form>
`, `,
) )
.add( .add(
'Pre Select', 'Pre Select',
() => html` () => html`
<lion-form> <lion-checkbox-group name="scientistsGroup" label="Favorite scientists">
<form> <lion-checkbox
<lion-checkbox-group name="scientistsGroup" label="Who are your favorite scientists?"> name="scientists[]"
<lion-checkbox label="Archimedes"
name="scientists[]" .choiceValue=${'Archimedes'}
label="Archimedes" ></lion-checkbox>
.choiceValue=${'Archimedes'} <lion-checkbox
></lion-checkbox> name="scientists[]"
<lion-checkbox label="Francis Bacon"
name="scientists[]" .choiceValue=${'Francis Bacon'}
label="Francis Bacon" checked
.choiceValue=${'Francis Bacon'} ></lion-checkbox>
checked <lion-checkbox
></lion-checkbox> name="scientists[]"
<lion-checkbox label="Marie Curie"
name="scientists[]" .modelValue=${{ value: 'Marie Curie', checked: true }}
label="Marie Curie" ></lion-checkbox>
.modelValue=${{ value: 'Marie Curie', checked: true }} </lion-checkbox-group>
></lion-checkbox>
</lion-checkbox-group>
</form>
</lion-form>
`, `,
) )
.add( .add(
'Disabled', 'Disabled',
() => html` () => html`
<lion-form> <lion-checkbox-group name="scientistsGroup" label="Favorite scientists" disabled>
<form> <lion-checkbox
<lion-checkbox-group name="scientists[]"
name="scientistsGroup" label="Archimedes"
label="Who are your favorite scientists?" .choiceValue=${'Archimedes'}
disabled ></lion-checkbox>
> <lion-checkbox
<lion-checkbox name="scientists[]"
name="scientists[]" label="Francis Bacon"
label="Archimedes" .choiceValue=${'Francis Bacon'}
.choiceValue=${'Archimedes'} ></lion-checkbox>
></lion-checkbox> <lion-checkbox
<lion-checkbox name="scientists[]"
name="scientists[]" label="Marie Curie"
label="Francis Bacon" .modelValue=${{ value: 'Marie Curie', checked: true }}
.choiceValue=${'Francis Bacon'} ></lion-checkbox>
></lion-checkbox> </lion-checkbox-group>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.modelValue=${{ value: 'Marie Curie', checked: true }}
></lion-checkbox>
</lion-checkbox-group>
</form>
</lion-form>
`, `,
) )
.add('Validation', () => { .add('Validation', () => {
const submit = () => { const validate = () => {
const form = document.querySelector('#form'); const checkboxGroup = document.querySelector('#scientistsGroup');
if (form.errorState === false) { checkboxGroup.submitted = !checkboxGroup.submitted;
console.log(form.serializeGroup());
}
}; };
return html` return html`
<lion-form id="form" @submit="${submit}" <lion-checkbox-group
><form> id="scientistsGroup"
<lion-checkbox-group name="scientistsGroup"
name="scientistsGroup" label="Favorite scientists"
label="Who are your favorite scientists?" .validators=${[new Required()]}
.errorValidators=${[['required']]}
>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>
</lion-checkbox-group>
<button type="submit">Submit</button>
</form></lion-form
> >
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>
</lion-checkbox-group>
<button @click="${() => validate()}">Validate</button>
`; `;
}) })
.add('Validation 2 checked', () => { .add('Validation 2 checked', () => {
const hasMinTwoChecked = value => { class HasMinTwoChecked extends Validator {
const selectedValues = value['scientists[]'].filter(v => v.checked === true); constructor(...args) {
return { super(...args);
hasMinTwoChecked: selectedValues.length >= 2, this.name = 'HasMinTwoChecked';
}; }
};
localize.locale = 'en-GB'; execute(value) {
try { let hasError = false;
localize.addData('en-GB', 'lion-validate+hasMinTwoChecked', { const selectedValues = value['scientists[]'].filter(v => v.checked === true);
error: { if (!(selectedValues.length >= 2)) {
hasMinTwoChecked: 'You need to select at least 2 values', hasError = true;
}, }
}); return hasError;
} catch (error) { }
// expected as it's a demo
static async getMessage() {
return 'You need to select at least 2 values.';
}
} }
const submit = () => { const validate = () => {
const form = document.querySelector('#form'); const checkboxGroup = document.querySelector('#scientistsGroup');
if (form.errorState === false) { checkboxGroup.submitted = !checkboxGroup.submitted;
console.log(form.serializeGroup());
}
}; };
return html` return html`
<lion-form id="form" @submit="${submit}" <lion-checkbox-group
><form> id="scientistsGroup"
<lion-checkbox-group name="scientistsGroup"
name="scientistsGroup" label="Favorite scientists"
label="Who are your favorite scientists?" help-text="You should have at least 2 of those"
help-text="You should have at least 2 of those" .validators=${[new Required(), new HasMinTwoChecked()]}
.errorValidators=${[[hasMinTwoChecked]]}
>
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>
</lion-checkbox-group>
<button type="submit">Submit</button>
</form></lion-form
> >
<lion-checkbox
name="scientists[]"
label="Archimedes"
.choiceValue=${'Archimedes'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Francis Bacon"
.choiceValue=${'Francis Bacon'}
></lion-checkbox>
<lion-checkbox
name="scientists[]"
label="Marie Curie"
.choiceValue=${'Marie Curie'}
></lion-checkbox>
</lion-checkbox-group>
<button @click="${() => validate()}">Validate</button>
`; `;
}); });

View file

@ -1,6 +1,7 @@
import { expect, html, fixture, nextFrame } from '@open-wc/testing'; import { expect, html, fixture, nextFrame } from '@open-wc/testing';
import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { localizeTearDown } from '@lion/localize/test-helpers.js';
import { Required } from '@lion/validate';
import '@lion/checkbox/lion-checkbox.js'; import '@lion/checkbox/lion-checkbox.js';
import '../lion-checkbox-group.js'; import '../lion-checkbox-group.js';
@ -12,15 +13,16 @@ beforeEach(() => {
describe('<lion-checkbox-group>', () => { describe('<lion-checkbox-group>', () => {
it('can be required', async () => { it('can be required', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-checkbox-group .errorValidators=${[['required']]}> <lion-checkbox-group .validators=${[new Required()]}>
<lion-checkbox name="sports[]" .choiceValue=${'running'}></lion-checkbox> <lion-checkbox name="sports[]" .choiceValue=${'running'}></lion-checkbox>
<lion-checkbox name="sports[]" .choiceValue=${'swimming'}></lion-checkbox> <lion-checkbox name="sports[]" .choiceValue=${'swimming'}></lion-checkbox>
</lion-checkbox-group> </lion-checkbox-group>
`); `);
await nextFrame(); await nextFrame();
expect(el.error.required).to.be.true; expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error.Required).to.be.true;
el.formElements['sports[]'][0].checked = true; el.formElements['sports[]'][0].checked = true;
expect(el.error.required).to.be.undefined; expect(el.hasFeedbackFor).to.deep.equal([]);
}); });
}); });

View file

@ -1,5 +1,16 @@
# Choice Input # ChoiceInputMixin
[//]: # 'AUTO INSERT HEADER PREPUBLISH' [//]: # 'AUTO INSERT HEADER PREPUBLISH'
We still need help writing better documentation - care to help? `lion-choice-input` mixin is a fundamental building block of form controls which return a checked-state. It is used in:
- [lion-checkbox](../checkbox/)
- [lion-option](../option/))
- [lion-radio](../radio/))
- [lion-switch](../switch/))
## Features
- Get or set the value of the choice - `choiceValue()`
- Get or set the modelValue (value and checked-state) of the choice - `.modelValue`
- Pre-select an option by setting the `checked` boolean attribute

View file

@ -37,6 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"@lion/input": "^0.2.1", "@lion/input": "^0.2.1",
"@lion/validate": "^0.3.1",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4", "@open-wc/testing": "^2.3.4",
"sinon": "^7.2.2" "sinon": "^7.2.2"

View file

@ -28,7 +28,7 @@ export const ChoiceInputMixin = superclass =>
hasChanged: (nw, old = {}) => nw.value !== old.value || nw.checked !== old.checked, hasChanged: (nw, old = {}) => nw.value !== old.value || nw.checked !== old.checked,
}, },
/** /**
* The value property of the modelValue. It provides an easy inteface for storing * The value property of the modelValue. It provides an easy interface for storing
* (complex) values in the modelValue * (complex) values in the modelValue
*/ */
choiceValue: { choiceValue: {
@ -200,13 +200,10 @@ export const ChoiceInputMixin = superclass =>
} }
/** /**
* @override * Used for required validator.
* Overridden from Field, since a different modelValue is used for choice inputs.
*/ */
__isRequired(modelValue) { _isEmpty() {
return { return !this.checked;
required: !!modelValue.checked,
};
} }
/** /**

View file

@ -1,6 +1,7 @@
import { expect, fixture } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from '@lion/core'; import { html } from '@lion/core';
import sinon from 'sinon'; import sinon from 'sinon';
import { Required } from '@lion/validate';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js'; import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js';
@ -86,12 +87,15 @@ describe('ChoiceInputMixin', () => {
it('can be required', async () => { it('can be required', async () => {
const el = await fixture(html` const el = await fixture(html`
<choice-input .choiceValue=${'foo'} .errorValidators=${[['required']]}></choice-input> <choice-input .choiceValue=${'foo'} .validators=${[new Required()]}></choice-input>
`); `);
expect(el.hasFeedbackFor).to.include('error');
expect(el.error.required).to.be.true; expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('Required');
el.checked = true; el.checked = true;
expect(el.error.required).to.be.undefined; expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
}); });
describe('Checked state synchronization', () => { describe('Checked state synchronization', () => {
@ -160,7 +164,7 @@ describe('ChoiceInputMixin', () => {
expect(spyModelCheckedToChecked.callCount).to.equal(1); expect(spyModelCheckedToChecked.callCount).to.equal(1);
expect(spyCheckedToModel.callCount).to.equal(1); expect(spyCheckedToModel.callCount).to.equal(1);
// not changeing values should not trigger any updates // not changing values should not trigger any updates
el.checked = false; el.checked = false;
el.modelValue = { value: 'foo', checked: false }; el.modelValue = { value: 'foo', checked: false };
expect(spyModelCheckedToChecked.callCount).to.equal(1); expect(spyModelCheckedToChecked.callCount).to.equal(1);
@ -177,8 +181,8 @@ describe('ChoiceInputMixin', () => {
`); `);
// Initial values // Initial values
expect(hasAttr(el)).to.equal(false, 'inital unchecked element'); expect(hasAttr(el)).to.equal(false, 'initial unchecked element');
expect(hasAttr(elChecked)).to.equal(true, 'inital checked element'); expect(hasAttr(elChecked)).to.equal(true, 'initial checked element');
// Programmatically via checked // Programmatically via checked
el.checked = true; el.checked = true;

View file

@ -61,8 +61,8 @@ In order to check whether the input is correct, an Application Developer can do
``` ```
```js ```js
function handleChange({ target: { modelValue, errorState } }) { function handleChange({ target: { modelValue, hasFeedbackFor } }) {
if (!(modelValue instanceof Unparseable) && !errorState) { if (!(modelValue instanceof Unparseable) && !(hasFeedbackFor.include('error))) {
// do my thing // do my thing
} }
} }

View file

@ -464,48 +464,6 @@ export const FormControlMixin = dedupeMixin(
]; ];
} }
// Extend validity showing conditions of ValidateMixin
showErrorCondition(newStates) {
return super.showErrorCondition(newStates) && this._interactionStateFeedbackCondition();
}
showWarningCondition(newStates) {
return super.showWarningCondition(newStates) && this._interactionStateFeedbackCondition();
}
showInfoCondition(newStates) {
return super.showInfoCondition(newStates) && this._interactionStateFeedbackCondition();
}
showSuccessCondition(newStates, oldStates) {
return (
super.showSuccessCondition(newStates, oldStates) &&
this._interactionStateFeedbackCondition()
);
}
_interactionStateFeedbackCondition() {
/**
* Show the validity feedback when one of the following conditions is met:
*
* - submitted
* If the form is submitted, always show the error message.
*
* - prefilled
* the user already filled in something, or the value is prefilled
* when the form is initially rendered.
*
* - touched && dirty && !prefilled
* When a user starts typing for the first time in a field with for instance `required`
* validation, error message should not be shown until a field becomes `touched`
* (a user leaves(blurs) a field).
* When a user enters a field without altering the value(making it `dirty`),
* an error message shouldn't be shown either.
*
*/
return (this.touched && this.dirty) || this.prefilled || this.submitted;
}
// aria-labelledby and aria-describedby helpers // aria-labelledby and aria-describedby helpers
// TODO: consider extracting to generic ariaLabel helper mixin // TODO: consider extracting to generic ariaLabel helper mixin

View file

@ -10,7 +10,7 @@ import { Unparseable } from '@lion/validate';
// - simplify _calculateValues: recursive trigger lock can be omitted, since need for connecting // - simplify _calculateValues: recursive trigger lock can be omitted, since need for connecting
// the loop via sync observers is not needed anymore. // the loop via sync observers is not needed anymore.
// - consider `formatOn` as an overridable function, by default something like: // - consider `formatOn` as an overridable function, by default something like:
// `(!__isHandlingUserInput || !errorState) && !focused` // `(!__isHandlingUserInput || !hasError) && !focused`
// This would allow for more advanced scenarios, like formatting an input whenever it becomes valid. // This would allow for more advanced scenarios, like formatting an input whenever it becomes valid.
// This would make formattedValue as a concept obsolete, since for maximum flexibility, the // This would make formattedValue as a concept obsolete, since for maximum flexibility, the
// formattedValue condition needs to be evaluated right before syncing back to the view // formattedValue condition needs to be evaluated right before syncing back to the view
@ -245,18 +245,24 @@ export const FormatMixin = dedupeMixin(
} }
__callFormatter() { __callFormatter() {
// - Why check for this.errorState? // - Why check for this.hasError?
// We only want to format values that are considered valid. For best UX, // We only want to format values that are considered valid. For best UX,
// we only 'reward' valid inputs. // we only 'reward' valid inputs.
// - Why check for __isHandlingUserInput? // - Why check for __isHandlingUserInput?
// Downwards sync is prevented whenever we are in an `@user-input-changed` flow, [2]. // Downwards sync is prevented whenever we are in an `@user-input-changed` flow, [2].
// If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back // If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back
// the value, no matter what. // the value, no matter what.
// This means, whenever we are in errorState and modelValue is set // This means, whenever we are in hasError and modelValue is set
// imperatively, we DO want to format a value (it is the only way to get meaningful // imperatively, we DO want to format a value (it is the only way to get meaningful
// input into `._inputNode` with modelValue as input) // input into `._inputNode` with modelValue as input)
if (this.__isHandlingUserInput && this.errorState && this._inputNode) { if (
this.__isHandlingUserInput &&
this.hasFeedbackFor &&
this.hasFeedbackFor.length &&
this.hasFeedbackFor.includes('error') &&
this._inputNode
) {
return this._inputNode ? this.value : undefined; return this._inputNode ? this.value : undefined;
} }

View file

@ -225,12 +225,14 @@ export class LionField extends FormControlMixin(
} }
} }
// eslint-disable-next-line class-methods-use-this set fieldName(value) {
__isRequired(modelValue) { this.__fieldName = value;
return { }
required:
(typeof modelValue === 'string' && modelValue !== '') || get fieldName() {
(typeof modelValue !== 'string' && typeof modelValue !== 'undefined'), // TODO: && modelValue !== null ? const label =
}; this.label ||
(this.querySelector('[slot=label]') && this.querySelector('[slot=label]').textContent);
return this.__fieldName || label || this.name;
} }
} }

View file

@ -2,7 +2,7 @@ import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-w
import sinon from 'sinon'; import sinon from 'sinon';
import { LitElement } from '@lion/core'; import { LitElement } from '@lion/core';
import { Unparseable } from '@lion/validate'; import { Unparseable, Validator } from '@lion/validate';
import { FormatMixin } from '../src/FormatMixin.js'; import { FormatMixin } from '../src/FormatMixin.js';
function mimicUserInput(formControl, newViewValue) { function mimicUserInput(formControl, newViewValue) {
@ -321,7 +321,7 @@ export function runFormatMixinSuite(customConfig) {
expect(el.modelValue).to.equal(''); expect(el.modelValue).to.equal('');
}); });
it('will only call the formatter for valid values on `user-input-changed` ', async () => { it.skip('will only call the formatter for valid values on `user-input-changed` ', async () => {
const formatterSpy = sinon.spy(value => `foo: ${value}`); const formatterSpy = sinon.spy(value => `foo: ${value}`);
const generatedModelValue = generateValueBasedOnType(); const generatedModelValue = generateValueBasedOnType();
@ -338,20 +338,30 @@ export function runFormatMixinSuite(customConfig) {
`); `);
expect(formatterSpy.callCount).to.equal(1); expect(formatterSpy.callCount).to.equal(1);
el.errorState = true; el.hasError = true;
// Ensure errorState is always true by putting a validator on it that always returns false. // Ensure hasError is always true by putting a validator on it that always returns false.
// Setting errorState = true is not enough if the element has errorValidators (uses ValidateMixin) // Setting hasError = true is not enough if the element has errorValidators (uses ValidateMixin)
// that set errorState back to false when the user input is mimicked. // that set hasError back to false when the user input is mimicked.
const alwaysInvalidator = () => ({ 'always-invalid': false });
el.errorValidators = [alwaysInvalidator]; const AlwaysInvalid = class extends Validator {
constructor(...args) {
super(...args);
this.name = 'AlwaysInvalid';
}
execute() {
return true;
}
};
el.validators = [new AlwaysInvalid()];
mimicUserInput(el, generatedViewValueAlt); mimicUserInput(el, generatedViewValueAlt);
expect(formatterSpy.callCount).to.equal(1); expect(formatterSpy.callCount).to.equal(1);
// Due to errorState, the formatter should not have ran. // Due to hasError, the formatter should not have ran.
expect(el.formattedValue).to.equal(generatedViewValueAlt); expect(el.formattedValue).to.equal(generatedViewValueAlt);
el.errorState = false; el.hasError = false;
el.errorValidators = []; el.validators = [];
mimicUserInput(el, generatedViewValue); mimicUserInput(el, generatedViewValue);
expect(formatterSpy.callCount).to.equal(2); expect(formatterSpy.callCount).to.equal(2);

View file

@ -9,6 +9,7 @@ import {
} from '@open-wc/testing'; } from '@open-wc/testing';
import { unsafeHTML } from '@lion/core'; import { unsafeHTML } from '@lion/core';
import sinon from 'sinon'; import sinon from 'sinon';
import { Validator, Required } from '@lion/validate';
import { localize } from '@lion/localize'; import { localize } from '@lion/localize';
import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { localizeTearDown } from '@lion/localize/test-helpers.js';
@ -35,6 +36,24 @@ describe('<lion-field>', () => {
expect(Array.from(el.children).find(child => child.slot === 'input').id).to.equal(el._inputId); expect(Array.from(el.children).find(child => child.slot === 'input').id).to.equal(el._inputId);
}); });
it(`has a fieldName based on the label`, async () => {
const el1 = await fixture(html`<${tag} label="foo">${inputSlot}</${tag}>`);
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
const el2 = await fixture(html`<${tag}><label slot="label">bar</label>${inputSlot}</${tag}>`);
expect(el2.fieldName).to.equal(el2._labelNode.textContent);
});
it(`has a fieldName based on the name if no label exists`, async () => {
const el = await fixture(html`<${tag} name="foo">${inputSlot}</${tag}>`);
expect(el.fieldName).to.equal(el.name);
});
it(`can override fieldName`, async () => {
const el = await fixture(html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlot}</${tag}>`);
expect(el.__fieldName).to.equal(el.fieldName);
});
it('fires focus/blur event on host and native input if focused/blurred', async () => { it('fires focus/blur event on host and native input if focused/blurred', async () => {
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`); const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
const cbFocusHost = sinon.spy(); const cbFocusHost = sinon.spy();
@ -284,66 +303,101 @@ describe('<lion-field>', () => {
}); });
}); });
it('shows validity states(error|warning|info|success) when interaction criteria met ', async () => { it('should conditionally show error', async () => {
// TODO: in order to make this test work as an integration test, we chose a modelValue const HasX = class extends Validator {
// that is compatible with lion-input-email. constructor() {
// However, when we can put priorities to validators (making sure error message of hasX is super();
// shown instead of a predefined validator like isEmail), we should fix this. this.name = 'HasX';
function hasX(str) { }
return { hasX: str.indexOf('x') > -1 };
}
const el = await fixture(html`<${tag}>${inputSlot}</${tag}>`);
const feedbackEl = el._feedbackElement;
el.modelValue = 'a@b.nl'; execute(value) {
el.errorValidators = [[hasX]]; const result = value.indexOf('x') === -1;
return result;
}
};
const el = await fixture(html`
<${tag}
.validators=${[new HasX()]}
.modelValue=${'a@b.nl'}
>
${inputSlot}
</${tag}>
`);
expect(el.error.hasX).to.equal(true); const executeScenario = async (_sceneEl, scenario) => {
expect(feedbackEl.innerText.trim()).to.equal( const sceneEl = _sceneEl;
'', sceneEl.resetInteractionState();
'shows no feedback, although the element has an error', sceneEl.touched = scenario.el.touched;
); sceneEl.dirty = scenario.el.dirty;
el.dirty = true; sceneEl.prefilled = scenario.el.prefilled;
el.touched = true; sceneEl.submitted = scenario.el.submitted;
el.modelValue = 'ab@c.nl'; // retrigger validation
await el.updateComplete;
expect(feedbackEl.innerText.trim()).to.equal( await sceneEl.updateComplete;
'This is error message for hasX', await sceneEl.feedbackComplete;
'shows feedback, because touched=true and dirty=true', expect(sceneEl.showsFeedbackFor).to.deep.equal(scenario.wantedShowsFeedbackFor);
); };
el.touched = false; await executeScenario(el, {
el.dirty = false; index: 0,
el.prefilled = true; el: { touched: true, dirty: true, prefilled: false, submitted: false },
await el.updateComplete; wantedShowsFeedbackFor: ['error'],
expect(feedbackEl.innerText.trim()).to.equal( });
'This is error message for hasX', await executeScenario(el, {
'shows feedback, because prefilled=true', index: 1,
); el: { touched: false, dirty: false, prefilled: true, submitted: false },
wantedShowsFeedbackFor: ['error'],
});
await executeScenario(el, {
index: 2,
el: { touched: false, dirty: false, prefilled: false, submitted: true },
wantedShowsFeedbackFor: ['error'],
});
await executeScenario(el, {
index: 3,
el: { touched: false, dirty: true, prefilled: false, submitted: false },
wantedShowsFeedbackFor: [],
});
await executeScenario(el, {
index: 4,
el: { touched: true, dirty: false, prefilled: false, submitted: false },
wantedShowsFeedbackFor: [],
});
}); });
it('can be required', async () => { it('can be required', async () => {
const el = await fixture(html` const el = await fixture(html`
<${tag} <${tag}
.errorValidators=${[['required']]} .validators=${[new Required()]}
>${inputSlot}</${tag}> >${inputSlot}</${tag}>
`); `);
expect(el.error.required).to.be.true; expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error).to.have.a.property('Required');
el.modelValue = 'cat'; el.modelValue = 'cat';
expect(el.error.required).to.be.undefined; expect(el.hasFeedbackFor).to.deep.equal([]);
expect(el.validationStates.error).not.to.have.a.property('Required');
}); });
it('will only update formattedValue when valid on `user-input-changed`', async () => { it('will only update formattedValue when valid on `user-input-changed`', async () => {
const formatterSpy = sinon.spy(value => `foo: ${value}`); const formatterSpy = sinon.spy(value => `foo: ${value}`);
function isBarValidator(value) { const Bar = class extends Validator {
return { isBar: value === 'bar' }; constructor(...args) {
} super(...args);
this.name = 'Bar';
}
execute(value) {
const hasError = value !== 'bar';
return hasError;
}
};
const el = await fixture(html` const el = await fixture(html`
<${tag} <${tag}
.modelValue=${'init-string'} .modelValue=${'init-string'}
.formatter=${formatterSpy} .formatter=${formatterSpy}
.errorValidators=${[[isBarValidator]]} .validators=${[new Bar()]}
>${inputSlot}</${tag}> >${inputSlot}</${tag}>
`); `);

View file

@ -35,7 +35,7 @@ import '@lion/input/lion-input.js';
```html ```html
<lion-fieldset name="personalia" label="personalia"> <lion-fieldset name="personalia" label="personalia">
<lion-input name="title" label="Title"></lion-input> <lion-input name="title" label="Title"></lion-input>
<lion-fieldset name="fullName" label="Full name" .errorValidations="${[['required]]}"> <lion-fieldset name="fullName" label="Full name" .validations="${[new Required()]}">
<lion-input name="firstName" label="First name"></lion-input> <lion-input name="firstName" label="First name"></lion-input>
<lion-input name="lastName" label="Last name"></lion-input> <lion-input name="lastName" label="Last name"></lion-input>
</lion-fieldset> </lion-fieldset>

View file

@ -0,0 +1,18 @@
import { Validator } from '@lion/validate';
export class FormElementsHaveNoError extends Validator {
constructor() {
super();
this.name = 'FormElementsHaveNoError';
}
// eslint-disable-next-line class-methods-use-this
execute(value, options, config) {
const hasError = config.node._anyFormElementHasFeedbackFor('error');
return hasError;
}
static async getMessage() {
return '';
}
}

View file

@ -2,9 +2,7 @@ import { SlotMixin, html, LitElement } from '@lion/core';
import { DisabledMixin } from '@lion/core/src/DisabledMixin.js'; import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
import { ValidateMixin } from '@lion/validate'; import { ValidateMixin } from '@lion/validate';
import { FormControlMixin, FormRegistrarMixin } from '@lion/field'; import { FormControlMixin, FormRegistrarMixin } from '@lion/field';
import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
// TODO: extract from module like import { pascalCase } from 'lion-element/CaseMapUtils.js'
const pascalCase = str => str.charAt(0).toUpperCase() + str.slice(1);
/** /**
* LionFieldset: fieldset wrapper providing extra features and integration with lion-field elements. * LionFieldset: fieldset wrapper providing extra features and integration with lion-field elements.
@ -88,6 +86,17 @@ export class LionFieldset extends FormRegistrarMixin(
}, []); }, []);
} }
set fieldName(value) {
this.__fieldName = value;
}
get fieldName() {
const label =
this.label ||
(this.querySelector('[slot=label]') && this.querySelector('[slot=label]').textContent);
return this.__fieldName || label || this.name;
}
constructor() { constructor() {
super(); super();
this.disabled = false; this.disabled = false;
@ -97,18 +106,20 @@ export class LionFieldset extends FormRegistrarMixin(
this.focused = false; this.focused = false;
this.formElements = {}; this.formElements = {};
this.__addedSubValidators = false; this.__addedSubValidators = false;
this.__createTypeAbsenceValidators();
this._checkForOutsideClick = this._checkForOutsideClick.bind(this); this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
this.addEventListener('focusin', this._syncFocused); this.addEventListener('focusin', this._syncFocused);
this.addEventListener('focusout', this._onFocusOut); this.addEventListener('focusout', this._onFocusOut);
this.addEventListener('validation-done', this.__validate);
this.addEventListener('dirty-changed', this._syncDirty); this.addEventListener('dirty-changed', this._syncDirty);
this.addEventListener('validate-performed', this.__validate);
this.defaultValidators = [new FormElementsHaveNoError()];
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); // eslint-disable-line wc/guard-super-call // eslint-disable-next-line wc/guard-super-call
super.connectedCallback();
this._setRole(); this._setRole();
} }
@ -230,20 +241,12 @@ export class LionFieldset extends FormRegistrarMixin(
}); });
} }
getValidatorsForType(type) {
const validators = super.getValidatorsForType(type) || [];
return [
...validators,
[this[`__formElementsHaveNo${pascalCase(type)}`], {}, { hideFeedback: true }],
];
}
_getFromAllFormElements(property) { _getFromAllFormElements(property) {
if (!this.formElements) { if (!this.formElements) {
return undefined; return undefined;
} }
const childrenNames = Object.keys(this.formElements); const childrenNames = Object.keys(this.formElements);
const values = childrenNames.length > 0 ? {} : undefined; const values = {};
childrenNames.forEach(name => { childrenNames.forEach(name => {
if (Array.isArray(this.formElements[name])) { if (Array.isArray(this.formElements[name])) {
// grouped via myName[] // grouped via myName[]
@ -284,6 +287,15 @@ export class LionFieldset extends FormRegistrarMixin(
}); });
} }
_anyFormElementHasFeedbackFor(state) {
return Object.keys(this.formElements).some(name => {
if (Array.isArray(this.formElements[name])) {
return this.formElements[name].some(el => !!el.hasFeedbackFor.includes(state));
}
return !!this.formElements[name].hasFeedbackFor.includes(state);
});
}
_everyFormElementHas(property) { _everyFormElementHas(property) {
return Object.keys(this.formElements).every(name => { return Object.keys(this.formElements).every(name => {
if (Array.isArray(this.formElements[name])) { if (Array.isArray(this.formElements[name])) {
@ -294,7 +306,7 @@ export class LionFieldset extends FormRegistrarMixin(
} }
/** /**
* Gets triggered by event 'validation-done' which enabled us to handle 2 different situations * Gets triggered by event 'validate-performed' which enabled us to handle 2 different situations
* - react on modelValue change, which says something about the validity as a whole * - react on modelValue change, which says something about the validity as a whole
* (at least two checkboxes for instance) and nothing about the children's values * (at least two checkboxes for instance) and nothing about the children's values
* - children validatity states have changed, so fieldset needs to update itself based on that * - children validatity states have changed, so fieldset needs to update itself based on that
@ -446,24 +458,4 @@ export class LionFieldset extends FormRegistrarMixin(
this.validate(); this.validate();
} }
/**
* Creates a validator for every type indicating whether all of the children formElements
* are not in the condition of {type} : i.e. __formElementsHaveNoError would be true if
* none of the children of the fieldset is in error state.
*/
__createTypeAbsenceValidators() {
this.constructor.validationTypes.forEach(type => {
this[`__formElementsHaveNo${pascalCase(type)}`] = () => ({
[`formElementsHaveNo${pascalCase(type)}`]: !this._anyFormElementHas(`${type}State`),
});
});
}
// eslint-disable-next-line class-methods-use-this
__isRequired() {
// eslint-disable-next-line no-console
console.warn(`Default "required" validator is not supported on fieldsets. If you have a valid
use case please let us know.`);
}
} }

View file

@ -1,11 +1,16 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import '../lion-fieldset.js'; import '../lion-fieldset.js';
import '@lion/input/lion-input.js';
import { localize } from '@lion/localize'; import { localize } from '@lion/localize';
import { minLengthValidator } from '@lion/validate';
import { Validator, MinLength, loadDefaultFeedbackMessages } from '@lion/validate';
import '../../form-system/stories/helper-wc/h-output.js'; import '../../form-system/stories/helper-wc/h-output.js';
localize.locale = 'en-GB';
loadDefaultFeedbackMessages();
storiesOf('Forms|Fieldset', module) storiesOf('Forms|Fieldset', module)
.add( .add(
'Default', 'Default',
@ -85,127 +90,119 @@ storiesOf('Forms|Fieldset', module)
`, `,
) )
.add('Validation', () => { .add('Validation', () => {
function isDemoValidator() { const DemoValidator = class extends Validator {
return false; constructor() {
} super();
this.name = 'DemoValidator';
}
const demoValidator = (...factoryParams) => [ execute(value) {
(...params) => ({ validator: isDemoValidator(...params) }), if (value && value.input1) {
...factoryParams, return true; // el.hasError = true
]; }
return false;
try { }
localize.addData('en-GB', 'lion-validate+validator', {
error: {
validator: 'Demo error message',
},
});
} catch (error) {
// expected as it's a demo
}
static async getMessage() {
return '[Fieldset Error] Demo error message';
}
};
return html` return html`
<lion-fieldset id="someId" .errorValidators=${[demoValidator()]}> <lion-fieldset id="someId" .validators="${[new DemoValidator()]}">
<lion-input name="input1" label="Label"></lion-input> <lion-input name="input1" label="Label"> </lion-input>
<button
@click=${() => {
document.getElementById('someId').serializeGroup();
}}
>
Submit
</button>
</lion-fieldset> </lion-fieldset>
<br />
<br />
<button>
Tab-able
</button>
`; `;
}) })
.add('Validation 2 inputs', () => { .add('Validation 2 inputs', () => {
const isCatsAndDogs = value => ({ const IsCatsAndDogs = class extends Validator {
isCatsAndDogs: value.input1 === 'cats' && value.input2 === 'dogs', constructor() {
}); super();
localize.locale = 'en-GB'; this.name = 'IsCatsAndDogs';
try { }
localize.addData('en-GB', 'lion-validate+isCatsAndDogs', {
error: {
isCatsAndDogs:
'[Fieldset Error] Input 1 needs to be "cats" and Input 2 needs to be "dogs"',
},
});
} catch (error) {
// expected as it's a demo
}
execute(value) {
if (!(value && value.input1 && value.input2)) {
return false;
}
return !(value.input1 === 'cats' && value.input2 === 'dogs');
}
static async getMessage() {
return '[Fieldset Error] Input 1 needs to be "cats" and Input 2 needs to be "dogs"';
}
};
return html` return html`
<lion-fieldset .errorValidators=${[[isCatsAndDogs]]}> <lion-fieldset .validators="${[new IsCatsAndDogs()]}">
<lion-input <lion-input
label="An all time YouTube favorite" label="An all time YouTube favorite"
name="input1" name="input1"
help-text="longer then 2 characters" help-text="longer than 2 characters"
.errorValidators=${[minLengthValidator(3)]} .validators="${[new MinLength(3)]}"
></lion-input> >
</lion-input>
<lion-input <lion-input
label="Another all time YouTube favorite" label="Another all time YouTube favorite"
name="input2" name="input2"
help-text="longer then 2 characters" help-text="longer than 2 characters"
.errorValidators=${[minLengthValidator(3)]} .validators="${[new MinLength(3)]}"
></lion-input> >
</lion-input>
</lion-fieldset> </lion-fieldset>
`; `;
}) })
.add('Validation 2 fieldsets', () => { .add('Validation 2 fields', () => {
const isCats = value => ({ const IsCats = class extends Validator {
isCats: value.input1 === 'cats', constructor() {
}); super();
localize.locale = 'en-GB'; this.name = 'IsCats';
try { }
localize.addData('en-GB', 'lion-validate+isCats', {
error: {
isCats: '[Fieldset Nr. 1 Error] Input 1 needs to be "cats"',
},
});
} catch (error) {
// expected as it's a demo
}
const isDogs = value => ({ execute(value) {
isDogs: value.input1 === 'dogs', return value.input1 !== 'cats';
}); }
localize.locale = 'en-GB';
try {
localize.addData('en-GB', 'lion-validate+isDogs', {
error: {
isDogs: '[Fieldset Nr. 2 Error] Input 1 needs to be "dogs"',
},
});
} catch (error) {
// expected as it's a demo
}
static async getMessage() {
return '[Fieldset Nr. 1 Error] Input 1 needs to be "cats"';
}
};
const IsDogs = class extends Validator {
constructor() {
super();
this.name = 'IsDogs';
}
execute(value) {
return value.input1 !== 'dogs';
}
static async getMessage() {
return '[Fieldset Nr. 2 Error] Input 1 needs to be "dogs"';
}
};
return html` return html`
<lion-fieldset .errorValidators=${[[isCats]]}> <lion-fieldset .validators="${[new IsCats()]}">
<label slot="label">Fieldset Nr. 1</label> <label slot="label">Fieldset no. 1</label>
<lion-input <lion-input
label="An all time YouTube favorite" label="An all time YouTube favorite"
name="input1" name="input1"
help-text="longer then 2 characters" help-text="longer than 2 characters"
.errorValidators=${[minLengthValidator(3)]} .validators="${[new MinLength(3)]}"
></lion-input> >
</lion-input>
</lion-fieldset> </lion-fieldset>
<br />
<hr /> <hr />
<br />
<lion-fieldset .errorValidators=${[[isDogs]]}> <lion-fieldset .validators="${[new IsDogs()]}">
<label slot="label">Fieldset Nr. 2</label> <label slot="label">Fieldset no. 2</label>
<lion-input <lion-input
label="An all time YouTube favorite" label="An all time YouTube favorite"
name="input1" name="input1"
help-text="longer then 2 characters" help-text="longer than 2 characters"
.errorValidators=${[minLengthValidator(3)]} .validators="${[new MinLength(3)]}"
></lion-input> >
</lion-input>
</lion-fieldset> </lion-fieldset>
`; `;
}); });

View file

@ -1,5 +1,6 @@
import { expect, fixture, html, unsafeStatic, triggerFocusFor, nextFrame } from '@open-wc/testing'; import { expect, fixture, html, unsafeStatic, triggerFocusFor, nextFrame } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import { Validator, IsNumber } from '@lion/validate';
import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { localizeTearDown } from '@lion/localize/test-helpers.js';
import '@lion/input/lion-input.js'; import '@lion/input/lion-input.js';
import '../lion-fieldset.js'; import '../lion-fieldset.js';
@ -21,14 +22,34 @@ beforeEach(() => {
}); });
describe('<lion-fieldset>', () => { describe('<lion-fieldset>', () => {
it(`has a fieldName based on the label`, async () => {
const el1 = await fixture(html`<${tag} label="foo">${inputSlots}</${tag}>`);
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
const el2 = await fixture(html`<${tag}><label slot="label">bar</label>${inputSlots}</${tag}>`);
expect(el2.fieldName).to.equal(el2._labelNode.textContent);
});
it(`has a fieldName based on the name if no label exists`, async () => {
const el = await fixture(html`<${tag} name="foo">${inputSlots}</${tag}>`);
expect(el.fieldName).to.equal(el.name);
});
it(`can override fieldName`, async () => {
const el = await fixture(
html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}>`,
);
expect(el.__fieldName).to.equal(el.fieldName);
});
it(`${tagString} has an up to date list of every form element in #formElements`, async () => { it(`${tagString} has an up to date list of every form element in #formElements`, async () => {
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`); const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame(); await nextFrame();
expect(Object.keys(fieldset.formElements).length).to.equal(3); expect(Object.keys(el.formElements).length).to.equal(3);
expect(fieldset.formElements['hobbies[]'].length).to.equal(2); expect(el.formElements['hobbies[]'].length).to.equal(2);
fieldset.removeChild(fieldset.formElements['hobbies[]'][0]); el.removeChild(el.formElements['hobbies[]'][0]);
expect(Object.keys(fieldset.formElements).length).to.equal(3); expect(Object.keys(el.formElements).length).to.equal(3);
expect(fieldset.formElements['hobbies[]'].length).to.equal(1); expect(el.formElements['hobbies[]'].length).to.equal(1);
}); });
it(`supports in html wrapped form elements`, async () => { it(`supports in html wrapped form elements`, async () => {
@ -46,17 +67,17 @@ describe('<lion-fieldset>', () => {
}); });
it('handles names with ending [] as an array', async () => { it('handles names with ending [] as an array', async () => {
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`); const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame(); await nextFrame();
fieldset.formElements['gender[]'][0].modelValue = { value: 'male' }; el.formElements['gender[]'][0].modelValue = { value: 'male' };
fieldset.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' }; el.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
fieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' }; el.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
expect(Object.keys(fieldset.formElements).length).to.equal(3); expect(Object.keys(el.formElements).length).to.equal(3);
expect(fieldset.formElements['hobbies[]'].length).to.equal(2); expect(el.formElements['hobbies[]'].length).to.equal(2);
expect(fieldset.formElements['hobbies[]'][0].modelValue.value).to.equal('chess'); expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess');
expect(fieldset.formElements['gender[]'][0].modelValue.value).to.equal('male'); expect(el.formElements['gender[]'][0].modelValue.value).to.equal('male');
expect(fieldset.modelValue['hobbies[]']).to.deep.equal([ expect(el.modelValue['hobbies[]']).to.deep.equal([
{ checked: false, value: 'chess' }, { checked: false, value: 'chess' },
{ checked: false, value: 'rugby' }, { checked: false, value: 'rugby' },
]); ]);
@ -124,36 +145,36 @@ describe('<lion-fieldset>', () => {
/* eslint-enable no-console */ /* eslint-enable no-console */
it('can dynamically add/remove elements', async () => { it('can dynamically add/remove elements', async () => {
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`); const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
const newField = await fixture(html`<${childTag} name="lastName"></${childTag}>`); const newField = await fixture(html`<${childTag} name="lastName"></${childTag}>`);
expect(Object.keys(fieldset.formElements).length).to.equal(3); expect(Object.keys(el.formElements).length).to.equal(3);
fieldset.appendChild(newField); el.appendChild(newField);
expect(Object.keys(fieldset.formElements).length).to.equal(4); expect(Object.keys(el.formElements).length).to.equal(4);
fieldset._inputNode.removeChild(newField); el._inputNode.removeChild(newField);
expect(Object.keys(fieldset.formElements).length).to.equal(3); expect(Object.keys(el.formElements).length).to.equal(3);
}); });
it('can read/write all values (of every input) via this.modelValue', async () => { it('can read/write all values (of every input) via this.modelValue', async () => {
const fieldset = await fixture(html` const el = await fixture(html`
<${tag}> <${tag}>
<${childTag} name="lastName"></${childTag}> <${childTag} name="lastName"></${childTag}>
<${tag} name="newfieldset">${inputSlots}</${tag}> <${tag} name="newfieldset">${inputSlots}</${tag}>
</${tag}> </${tag}>
`); `);
await fieldset.registrationReady; await el.registrationReady;
const newFieldset = fieldset.querySelector('lion-fieldset'); const newFieldset = el.querySelector('lion-fieldset');
await newFieldset.registrationReady; await newFieldset.registrationReady;
fieldset.formElements.lastName.modelValue = 'Bar'; el.formElements.lastName.modelValue = 'Bar';
newFieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'chess' }; newFieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'chess' };
newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'football' }; newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'football' };
newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; newFieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
expect(fieldset.modelValue).to.deep.equal({ expect(el.modelValue).to.deep.equal({
lastName: 'Bar', lastName: 'Bar',
newfieldset: { newfieldset: {
'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'football' }], 'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'football' }],
@ -161,7 +182,7 @@ describe('<lion-fieldset>', () => {
color: { checked: false, value: 'blue' }, color: { checked: false, value: 'blue' },
}, },
}); });
fieldset.modelValue = { el.modelValue = {
lastName: 2, lastName: 2,
newfieldset: { newfieldset: {
'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'baseball' }], 'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'baseball' }],
@ -177,7 +198,7 @@ describe('<lion-fieldset>', () => {
checked: false, checked: false,
value: 'baseball', value: 'baseball',
}); });
expect(fieldset.formElements.lastName.modelValue).to.equal(2); expect(el.formElements.lastName.modelValue).to.equal(2);
}); });
it('does not throw if setter data of this.modelValue can not be handled', async () => { it('does not throw if setter data of this.modelValue can not be handled', async () => {
@ -204,9 +225,9 @@ describe('<lion-fieldset>', () => {
it('disables/enables all its formElements if it becomes disabled/enabled', async () => { it('disables/enables all its formElements if it becomes disabled/enabled', async () => {
const el = await fixture(html`<${tag} disabled>${inputSlots}</${tag}>`); const el = await fixture(html`<${tag} disabled>${inputSlots}</${tag}>`);
await nextFrame(); await nextFrame();
expect(el.formElements.color.disabled).to.equal(true); expect(el.formElements.color.disabled).to.be.true;
expect(el.formElements['hobbies[]'][0].disabled).to.equal(true); expect(el.formElements['hobbies[]'][0].disabled).to.be.true;
expect(el.formElements['hobbies[]'][1].disabled).to.equal(true); expect(el.formElements['hobbies[]'][1].disabled).to.be.true;
el.disabled = false; el.disabled = false;
await el.updateComplete; await el.updateComplete;
@ -221,27 +242,36 @@ describe('<lion-fieldset>', () => {
); );
await el.updateComplete; await el.updateComplete;
expect(el.disabled).to.equal(false); expect(el.disabled).to.equal(false);
expect(el.formElements.sub.disabled).to.equal(true); expect(el.formElements.sub.disabled).to.be.true;
expect(el.formElements.sub.formElements.color.disabled).to.equal(true); expect(el.formElements.sub.formElements.color.disabled).to.be.true;
expect(el.formElements.sub.formElements['hobbies[]'][0].disabled).to.equal(true); expect(el.formElements.sub.formElements['hobbies[]'][0].disabled).to.be.true;
expect(el.formElements.sub.formElements['hobbies[]'][1].disabled).to.equal(true); expect(el.formElements.sub.formElements['hobbies[]'][1].disabled).to.be.true;
}); });
describe('validation', () => { describe('validation', () => {
it('validates on init', async () => { it('validates on init', async () => {
function isCat(value) { class IsCat extends Validator {
return { isCat: value === 'cat' }; constructor() {
super();
this.name = 'IsCat';
}
execute(value) {
const hasError = value !== 'cat';
return hasError;
}
} }
const el = await fixture(html` const el = await fixture(html`
<${tag}> <${tag}>
<${childTag} name="color" <${childTag} name="color"
.errorValidators=${[[isCat]]} .validators=${[new IsCat()]}
.modelValue=${'blue'} .modelValue=${'blue'}
></${childTag}> ></${childTag}>
</${tag}> </${tag}>
`); `);
await nextFrame(); await nextFrame();
expect(el.formElements.color.error.isCat).to.equal(true); expect(el.formElements.color.validationStates.error.IsCat).to.be.true;
}); });
it('validates when a value changes', async () => { it('validates when a value changes', async () => {
@ -252,57 +282,70 @@ describe('<lion-fieldset>', () => {
expect(spy.callCount).to.equal(1); expect(spy.callCount).to.equal(1);
}); });
it('has a special {error, warning, info, success} validator for all children - can be checked via this.error.formElementsHaveNoError', async () => { it('has a special validator for all children - can be checked via this.error.FormElementsHaveNoError', async () => {
function isCat(value) { class IsCat extends Validator {
return { isCat: value === 'cat' }; constructor() {
super();
this.name = 'IsCat';
}
execute(value) {
const hasError = value !== 'cat';
return hasError;
}
} }
const el = await fixture(html` const el = await fixture(html`
<${tag}> <${tag}>
<${childTag} name="color" <${childTag} name="color"
.errorValidators=${[[isCat]]} .validators=${[new IsCat()]}
.modelValue=${'blue'} .modelValue=${'blue'}
></${childTag}> ></${childTag}>
</${tag}> </${tag}>
`); `);
await nextFrame(); await nextFrame();
expect(el.error.formElementsHaveNoError).to.equal(true); expect(el.validationStates.error.FormElementsHaveNoError).to.be.true;
expect(el.formElements.color.error.isCat).to.equal(true); expect(el.formElements.color.validationStates.error.IsCat).to.be.true;
el.formElements.color.modelValue = 'cat'; el.formElements.color.modelValue = 'cat';
expect(el.error).to.deep.equal({}); expect(el.validationStates.error).to.deep.equal({});
}); });
it('validates on children (de)registration', async () => { it('validates on children (de)registration', async () => {
function hasEvenNumberOfChildren(modelValue) { class HasEvenNumberOfChildren extends Validator {
return { even: Object.keys(modelValue).length % 2 === 0 }; constructor() {
super();
this.name = 'HasEvenNumberOfChildren';
}
execute(value) {
const hasError = Object.keys(value).length % 2 !== 0;
return hasError;
}
} }
const el = await fixture(html` const el = await fixture(html`
<${tag} .errorValidators=${[[hasEvenNumberOfChildren]]}> <${tag} .validators=${[new HasEvenNumberOfChildren()]}>
<${childTag} id="c1" name="c1"></${childTag}> <${childTag} id="c1" name="c1"></${childTag}>
</${tag}> </${tag}>
`); `);
const child2 = await fixture( const child2 = await fixture(html`
html` <${childTag} name="c2"></${childTag}>
<${childTag} name="c2"></${childTag}> `);
`,
);
await nextFrame(); await nextFrame();
expect(el.error.even).to.equal(true); expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true;
el.appendChild(child2); el.appendChild(child2);
await nextFrame(); await nextFrame();
expect(el.error.even).to.equal(undefined); expect(el.validationStates.error.HasEvenNumberOfChildren).to.equal(undefined);
el.removeChild(child2); el.removeChild(child2);
await nextFrame(); await nextFrame();
expect(el.error.even).to.equal(true); expect(el.validationStates.error.HasEvenNumberOfChildren).to.be.true;
// Edge case: remove all children // Edge case: remove all children
el.removeChild(el.querySelector('[id=c1]')); el.removeChild(el.querySelector('[id=c1]'));
await nextFrame(); await nextFrame();
expect(el.error.even).to.equal(undefined); expect(el.validationStates.error.HasEvenNumberOfChildren).to.equal(undefined);
}); });
}); });
@ -319,7 +362,7 @@ describe('<lion-fieldset>', () => {
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`); const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
await nextFrame(); await nextFrame();
fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' }; fieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'football' };
expect(fieldset.dirty).to.equal(true); expect(fieldset.dirty).to.be.true;
}); });
it('sets touched when last field in fieldset left after focus', async () => { it('sets touched when last field in fieldset left after focus', async () => {
@ -426,14 +469,24 @@ describe('<lion-fieldset>', () => {
}); });
it('potentially shows fieldset error message on interaction change', async () => { it('potentially shows fieldset error message on interaction change', async () => {
const input1IsTen = value => ({ input1IsTen: value.input1 === 10 }); class Input1IsTen extends Validator {
const isNumber = value => ({ isNumber: typeof value === 'number' }); constructor() {
super();
this.name = 'Input1IsTen';
}
execute(value) {
const hasError = value.input1 !== 10;
return hasError;
}
}
const outSideButton = await fixture(html` const outSideButton = await fixture(html`
<button>outside</button> <button>outside</button>
`); `);
const el = await fixture(html` const el = await fixture(html`
<${tag} .errorValidators=${[[input1IsTen]]}> <${tag} .validators=${[new Input1IsTen()]}>
<${childTag} name="input1" .errorValidators=${[[isNumber]]}></${childTag}> <${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}>
</${tag}> </${tag}>
`); `);
await nextFrame(); await nextFrame();
@ -443,20 +496,29 @@ describe('<lion-fieldset>', () => {
outSideButton.focus(); outSideButton.focus();
await el.updateComplete; await el.updateComplete;
expect(el.error.input1IsTen).to.be.true; expect(el.validationStates.error.Input1IsTen).to.be.true;
expect(el.errorShow).to.be.true; expect(el.showsFeedbackFor).to.deep.equal(['error']);
}); });
it('show error if tabbing "out" of last ', async () => { it('show error if tabbing "out" of last ', async () => {
const input1IsTen = value => ({ input1IsTen: value.input1 === 10 }); class Input1IsTen extends Validator {
const isNumber = value => ({ isNumber: typeof value === 'number' }); constructor() {
super();
this.name = 'Input1IsTen';
}
execute(value) {
const hasError = value.input1 !== 10;
return hasError;
}
}
const outSideButton = await fixture(html` const outSideButton = await fixture(html`
<button>outside</button> <button>outside</button>
`); `);
const el = await fixture(html` const el = await fixture(html`
<${tag} .errorValidators=${[[input1IsTen]]}> <${tag} .validators=${[new Input1IsTen()]}>
<${childTag} name="input1" .errorValidators=${[[isNumber]]}></${childTag}> <${childTag} name="input1" .validators=${[new IsNumber()]}></${childTag}>
<${childTag} name="input2" .errorValidators=${[[isNumber]]}></${childTag}> <${childTag} name="input2" .validators=${[new IsNumber()]}></${childTag}>
</${tag}> </${tag}>
`); `);
const inputs = el.querySelectorAll(childTagString); const inputs = el.querySelectorAll(childTagString);
@ -466,8 +528,8 @@ describe('<lion-fieldset>', () => {
outSideButton.focus(); outSideButton.focus();
await nextFrame(); await nextFrame();
expect(el.error.input1IsTen).to.be.true; expect(el.validationStates.error.Input1IsTen).to.be.true;
expect(el.errorShow).to.be.true; expect(el.hasFeedbackFor).to.deep.equal(['error']);
}); });
}); });
@ -711,34 +773,52 @@ describe('<lion-fieldset>', () => {
}); });
it('has correct validation afterwards', async () => { it('has correct validation afterwards', async () => {
const isCat = modelValue => ({ isCat: modelValue === 'cat' }); class IsCat extends Validator {
const containsA = modelValues => ({ constructor() {
containsA: modelValues.color ? modelValues.color.indexOf('a') > -1 : false, super();
}); this.name = 'IsCat';
}
execute(value) {
const hasError = value !== 'cat';
return hasError;
}
}
class ColorContainsA extends Validator {
constructor() {
super();
this.name = 'ColorContainsA';
}
execute(value) {
const hasError = value.color.indexOf('a') === -1;
return hasError;
}
}
const el = await fixture(html` const el = await fixture(html`
<${tag} .errorValidators=${[[containsA]]}> <${tag} .validators=${[new ColorContainsA()]}>
<${childTag} name="color" .errorValidators=${[[isCat]]}></${childTag}> <${childTag} name="color" .validators=${[new IsCat()]}></${childTag}>
<${childTag} name="color2"></${childTag}> <${childTag} name="color2"></${childTag}>
</${tag}> </${tag}>
`); `);
await el.registrationReady; await el.registrationReady;
expect(el.errorState).to.be.true; expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.error.containsA).to.be.true; expect(el.validationStates.error.ColorContainsA).to.be.true;
expect(el.formElements.color.errorState).to.be.false; expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]);
el.formElements.color.modelValue = 'onlyb'; el.formElements.color.modelValue = 'onlyb';
expect(el.errorState).to.be.true; expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.error.containsA).to.be.true; expect(el.validationStates.error.ColorContainsA).to.be.true;
expect(el.formElements.color.error.isCat).to.be.true; expect(el.formElements.color.validationStates.error.IsCat).to.be.true;
el.formElements.color.modelValue = 'cat'; el.formElements.color.modelValue = 'cat';
expect(el.errorState).to.be.false; expect(el.hasFeedbackFor).to.deep.equal([]);
el.resetGroup(); el.resetGroup();
expect(el.errorState).to.be.true; expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.error.containsA).to.be.true; expect(el.validationStates.error.ColorContainsA).to.be.true;
expect(el.formElements.color.errorState).to.be.false; expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]);
}); });
it('has access to `_initialModelValue` based on initial children states', async () => { it('has access to `_initialModelValue` based on initial children states', async () => {
@ -817,7 +897,7 @@ describe('<lion-fieldset>', () => {
fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' }; fieldset.formElements['gender[]'][0].modelValue = { checked: false, value: 'male' };
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' }; fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' }; fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
expect(fieldset.hasAttribute('role')).to.equal(true); expect(fieldset.hasAttribute('role')).to.be.true;
expect(fieldset.getAttribute('role')).to.contain('group'); expect(fieldset.getAttribute('role')).to.contain('group');
}); });

View file

@ -13,6 +13,7 @@ import '@lion/input-iban/lion-input-iban.js';
import '@lion/input-amount/lion-input-amount.js'; import '@lion/input-amount/lion-input-amount.js';
import '@lion/input-date/lion-input-date.js'; import '@lion/input-date/lion-input-date.js';
import '@lion/input-email/lion-input-email.js'; import '@lion/input-email/lion-input-email.js';
import { Required, MinLength } from '@lion/validate';
storiesOf('Forms|Form', module).add( storiesOf('Forms|Form', module).add(
'Umbrella form', 'Umbrella form',
@ -22,12 +23,12 @@ storiesOf('Forms|Form', module).add(
<lion-input <lion-input
name="first_name" name="first_name"
label="First Name" label="First Name"
.errorValidators="${['required']}" .validators="${[new Required()]}"
></lion-input> ></lion-input>
<lion-input <lion-input
name="last_name" name="last_name"
label="Last Name" label="Last Name"
.errorValidators="${['required']}" .validators="${[new Required()]}"
></lion-input> ></lion-input>
<!-- TODO: lion-input-birthdate --> <!-- TODO: lion-input-birthdate -->
@ -36,13 +37,13 @@ storiesOf('Forms|Form', module).add(
name="date" name="date"
label="Date of application" label="Date of application"
.modelValue="${'2000-12-12'}" .modelValue="${'2000-12-12'}"
.errorValidators="${['required']}" .validators="${[new Required()]}"
></lion-input-date> ></lion-input-date>
<lion-textarea <lion-textarea
name="bio" name="bio"
label="Biography" label="Biography"
.errorValidators="${['required']}" .validators="${[new Required(), new MinLength(10)]}"
help-text="Please enter at least 10 characters" help-text="Please enter at least 10 characters"
></lion-textarea> ></lion-textarea>
@ -50,7 +51,11 @@ storiesOf('Forms|Form', module).add(
<lion-input-iban name="iban" label="Iban"></lion-input-iban> <lion-input-iban name="iban" label="Iban"></lion-input-iban>
<lion-input-email name="email" label="Email"></lion-input-email> <lion-input-email name="email" label="Email"></lion-input-email>
<lion-checkbox-group min-selected="0" label="What do you like?" name="checkers"> <lion-checkbox-group
label="What do you like?"
name="checkers"
.validators="${[new Required()]}"
>
<lion-checkbox name="checkers[]" value="foo" label="I like foo"></lion-checkbox> <lion-checkbox name="checkers[]" value="foo" label="I like foo"></lion-checkbox>
<lion-checkbox name="checkers[]" value="bar" label="I like bar"></lion-checkbox> <lion-checkbox name="checkers[]" value="bar" label="I like bar"></lion-checkbox>
<lion-checkbox name="checkers[]" value="baz" label="I like baz"></lion-checkbox> <lion-checkbox name="checkers[]" value="baz" label="I like baz"></lion-checkbox>
@ -59,8 +64,8 @@ storiesOf('Forms|Form', module).add(
<lion-radio-group <lion-radio-group
class="vertical" class="vertical"
name="dinosaurs" name="dinosaurs"
label="What is your favorite dinosaur?" label="Favorite dinosaur"
.errorValidators="${['required']}" .validators="${[new Required()]}"
error-message="Dinosaurs error message" error-message="Dinosaurs error message"
> >
<lion-radio name="dinosaurs[]" value="allosaurus" label="allosaurus"></lion-radio> <lion-radio name="dinosaurs[]" value="allosaurus" label="allosaurus"></lion-radio>
@ -73,7 +78,7 @@ storiesOf('Forms|Form', module).add(
<lion-select <lion-select
label="Make a selection (rich select)" label="Make a selection (rich select)"
name="lyrics" name="lyrics"
.errorValidators="${['required']}" .validators="${[new Required()]}"
> >
<select slot="input"> <select slot="input">
<option value="1">Fire up that loud</option> <option value="1">Fire up that loud</option>
@ -82,7 +87,7 @@ storiesOf('Forms|Form', module).add(
</select> </select>
</lion-select> </lion-select>
<lion-checkbox-group name="terms"> <lion-checkbox-group name="terms" .validators="${[new Required()]}">
<lion-checkbox <lion-checkbox
name="terms[]" name="terms[]"
label="I blindly accept all terms and conditions" label="I blindly accept all terms and conditions"

View file

@ -1,11 +1,11 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { render } from '@lion/core'; import { render } from '@lion/core';
import { localize } from '@lion/localize';
import '@lion/checkbox/lion-checkbox.js'; import '@lion/checkbox/lion-checkbox.js';
import '@lion/checkbox-group/lion-checkbox-group.js'; import '@lion/checkbox-group/lion-checkbox-group.js';
import '@lion/form/lion-form.js'; import '@lion/form/lion-form.js';
import '@lion/input/lion-input.js'; import '@lion/input/lion-input.js';
import './helper-wc/h-output.js'; import './helper-wc/h-output.js';
import { Validator } from '@lion/validate';
function renderOffline(litHtmlTemplate) { function renderOffline(litHtmlTemplate) {
const offlineRenderContainer = document.createElement('div'); const offlineRenderContainer = document.createElement('div');
@ -13,12 +13,6 @@ function renderOffline(litHtmlTemplate) {
return offlineRenderContainer.firstElementChild; return offlineRenderContainer.firstElementChild;
} }
function addTranslations(ns, data) {
if (!localize._isNamespaceInCache('en-GB', ns)) {
localize.addData('en-GB', ns, data);
}
}
storiesOf('Form Fundaments|Interaction States', module) storiesOf('Form Fundaments|Interaction States', module)
.add( .add(
'States', 'States',
@ -46,26 +40,39 @@ storiesOf('Form Fundaments|Interaction States', module)
// 2. Create a validator... // 2. Create a validator...
// Define a demo validator that should only be visible on an odd amount of characters // Define a demo validator that should only be visible on an odd amount of characters
const oddValidator = [modelValue => ({ odd: modelValue.length % 2 !== 0 })]; // const OddValidator = [modelValue => ({ odd: modelValue.length % 2 !== 0 })];
class OddValidator extends Validator {
constructor(...args) {
super(...args);
this.name = 'OddValidator';
}
addTranslations('lion-validate+odd', { // eslint-disable-next-line class-methods-use-this
error: { execute(value) {
odd: '[ Error feedback ] : Add or remove one character', let hasError = false;
}, if (!(value.length % 2 !== 0)) {
}); hasError = true;
}
return hasError;
}
_getMessage() {
return 'Add or remove one character';
}
}
// 3. Create field overriding .showErrorCondition... // 3. Create field overriding .showErrorCondition...
// Here we will store a reference to the Field element that overrides the default condition // Here we will store a reference to the Field element that overrides the default condition
// (function `showErrorCondition`) for triggering validation feedback of `.errorValidators` // (function `showErrorCondition`) for triggering validation feedback of `.validators`
const fieldElement = renderOffline(html` const fieldElement = renderOffline(html`
<lion-input <lion-input
name="interactionField" name="interactionField"
label="Only an odd amount of characters allowed" label="Only an odd amount of characters allowed"
help-text="Change feedback condition" help-text="Change feedback condition"
.modelValue="${'notodd'}" .modelValue="${'notodd'}"
.errorValidators="${[oddValidator]}" .validators="${[new OddValidator()]}"
.showErrorCondition="${newStates => .showErrorCondition="${newStates =>
newStates.error && conditions.every(p => fieldElement[p])}" newStates.errorStates && conditions.every(p => fieldElement[p])}"
> >
<input slot="input" /> <input slot="input" />
</lion-input> </lion-input>
@ -89,7 +96,7 @@ storiesOf('Form Fundaments|Interaction States', module)
</form> </form>
</lion-form> </lion-form>
<h-output .field="${fieldElement}" .show="${[...props, 'errorState']}"> </h-output> <h-output .field="${fieldElement}" .show="${[...props, 'hasFeedbackFor']}"> </h-output>
<h3> <h3>
Set conditions for validation feedback visibility Set conditions for validation feedback visibility

View file

@ -5,7 +5,7 @@ import '@lion/fieldset/lion-fieldset.js';
import '@lion/input-iban/lion-input-iban.js'; import '@lion/input-iban/lion-input-iban.js';
import '@lion/textarea/lion-textarea.js'; import '@lion/textarea/lion-textarea.js';
import { maxLengthValidator } from '@lion/validate'; import { Required, MaxLength } from '@lion/validate';
storiesOf('Forms|Form', module) storiesOf('Forms|Form', module)
.add( .add(
@ -39,7 +39,7 @@ storiesOf('Forms|Form', module)
.add('Form Submit/Reset', () => { .add('Form Submit/Reset', () => {
const submit = () => { const submit = () => {
const form = document.querySelector('#form'); const form = document.querySelector('#form');
if (form.errorState === false) { if (!form.hasFeedbackFor.includes('error')) {
console.log(form.serializeGroup()); console.log(form.serializeGroup());
} }
}; };
@ -50,13 +50,13 @@ storiesOf('Forms|Form', module)
<lion-input <lion-input
name="firstName" name="firstName"
label="First Name" label="First Name"
.errorValidators=${['required', maxLengthValidator(15)]} .validators=${[new Required(), new MaxLength(15)]}
> >
</lion-input> </lion-input>
<lion-input <lion-input
name="lastName" name="lastName"
label="Last Name" label="Last Name"
.errorValidators=${['required', maxLengthValidator(15)]} .validators=${[new Required(), new MaxLength(15)]}
> >
</lion-input> </lion-input>
</lion-fieldset> </lion-fieldset>

View file

@ -12,11 +12,11 @@
- option to override locale to change the formatting and parsing - option to override locale to change the formatting and parsing
- option to provide additional format options overrides - option to provide additional format options overrides
- default label in different languages - default label in different languages
- can make use of number specific [validators](../validate/docs/DefaultValidators.md) with corresponding error messages in different languages - can make use of number specific [validators](../validate/docs/ValidationSystem.md) with corresponding error messages in different languages
- isNumber (default) - IsNumber (default)
- minNumber - MinNumber
- maxNumber - MaxNumber
- minMaxNumber - MinMaxNumber
## How to use ## How to use
@ -30,7 +30,7 @@ npm i --save @lion/input-amount
import '@lion/input-amount/lion-input-amount.js'; import '@lion/input-amount/lion-input-amount.js';
// validator import example // validator import example
import { minNumberValidator } from '@lion/validate'; import { Required, MinNumber } from '@lion/validate';
``` ```
### Example ### Example
@ -39,6 +39,6 @@ import { minNumberValidator } from '@lion/validate';
<lion-input-amount <lion-input-amount
name="amount" name="amount"
currency="USD" currency="USD"
.errorValidators="${[['required'], minNumberValidator(100)]}" .validators="${[new Required(), new MinNumber(100)]}"
></lion-input-amount> ></lion-input-amount>
``` ```

View file

@ -2,7 +2,7 @@ import { css } from '@lion/core';
import { LocalizeMixin } from '@lion/localize'; import { LocalizeMixin } from '@lion/localize';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import { FieldCustomMixin } from '@lion/field'; import { FieldCustomMixin } from '@lion/field';
import { isNumberValidator } from '@lion/validate'; import { IsNumber } from '@lion/validate';
import { parseAmount } from './parsers.js'; import { parseAmount } from './parsers.js';
import { formatAmount } from './formatters.js'; import { formatAmount } from './formatters.js';
@ -46,6 +46,8 @@ export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput))
super(); super();
this.parser = parseAmount; this.parser = parseAmount;
this.formatter = formatAmount; this.formatter = formatAmount;
this.defaultValidators.push(new IsNumber());
} }
connectedCallback() { connectedCallback() {
@ -62,13 +64,6 @@ export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput))
this._calculateValues(); this._calculateValues();
} }
getValidatorsForType(type) {
if (type === 'error') {
return [isNumberValidator()].concat(super.getValidatorsForType(type) || []);
}
return super.getValidatorsForType(type);
}
static get styles() { static get styles() {
return [ return [
...super.styles, ...super.styles,

View file

@ -1,23 +1,21 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { Required } from '@lion/validate';
import '../lion-input-amount.js'; import '../lion-input-amount.js';
storiesOf('Forms|Input Amount', module) storiesOf('Forms|Input Amount', module)
.add( .add(
'Default', 'Default',
() => html` () => html`
<lion-input-amount .errorValidators="${['required']}" label="Amount" .modelValue=${123456.78}> <lion-input-amount label="Amount" .validators="${[new Required()]}" .modelValue=${123456.78}>
</lion-input-amount> </lion-input-amount>
`, `,
) )
.add( .add(
'Negative number', 'Negative number',
() => html` () => html`
<lion-input-amount <lion-input-amount label="Amount" .validators="${[new Required()]}" .modelValue=${-123456.78}>
.errorValidators="${['required']}"
label="Amount"
.modelValue=${-123456.78}
>
</lion-input-amount> </lion-input-amount>
`, `,
) )
@ -36,12 +34,8 @@ storiesOf('Forms|Input Amount', module)
.add( .add(
'Force locale to nl-NL', 'Force locale to nl-NL',
() => html` () => html`
<lion-input-amount <lion-input-amount label="Price" currency="JOD">
.formatOptions="${{ locale: 'nl-NL' }}" .formatOptions="${{ locale: 'nl-NL' }}" .modelValue=${123456.78}
label="Price"
.modelValue=${123456.78}
currency="JOD"
>
</lion-input-amount> </lion-input-amount>
`, `,
) )
@ -49,10 +43,10 @@ storiesOf('Forms|Input Amount', module)
'Force locale to en-US', 'Force locale to en-US',
() => html` () => html`
<lion-input-amount <lion-input-amount
.formatOptions="${{ locale: 'en-US' }}"
label="Price" label="Price"
.modelValue=${123456.78}
currency="YEN" currency="YEN"
.formatOptions="${{ locale: 'en-US' }}"
.modelValue=${123456.78}
> >
</lion-input-amount> </lion-input-amount>
`, `,

View file

@ -10,11 +10,11 @@
- makes use of [formatDate](../localize/docs/date.md) for formatting and parsing. - makes use of [formatDate](../localize/docs/date.md) for formatting and parsing.
- option to overwrite locale to change the formatting and parsing - option to overwrite locale to change the formatting and parsing
- default label in different languages - default label in different languages
- can make use of date specific [validators](../validate/docs/DefaultValidators.md) with corresponding error messages in different languages - can make use of date specific [validators](../validate/docs/ValidationSystem.md) with corresponding error messages in different languages
- isDate (default) - IsDate (default)
- minDate - MinDate
- maxDate - MaxDate
- minMaxDate - MinMaxDate
## How to use ## How to use
@ -28,7 +28,7 @@ npm i --save @lion/input-date
import '@lion/input-date/lion-input-date.js'; import '@lion/input-date/lion-input-date.js';
// validator import example // validator import example
import { minDateValidator } from '@lion/validate'; import { Required, MinDate } from '@lion/validate';
``` ```
### Example ### Example
@ -36,6 +36,6 @@ import { minDateValidator } from '@lion/validate';
```html ```html
<lion-input-date <lion-input-date
name="date" name="date"
.errorValidators="${[['required'], minDateValidator(100)]}" .validators="${[new Required(), new MinDate(new Date('2018/05/24'))]}"
></lion-input-date> ></lion-input-date>
``` ```

View file

@ -1,19 +1,19 @@
import { LocalizeMixin, formatDate, parseDate } from '@lion/localize'; import { LocalizeMixin, formatDate, parseDate } from '@lion/localize';
import { FieldCustomMixin } from '@lion/field'; import { FieldCustomMixin } from '@lion/field';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import { isDateValidator } from '@lion/validate'; import { IsDate } from '@lion/validate';
/** /**
* `LionInputDate` is a class for a date custom form element (`<lion-input-date>`). * `LionInputDate` has a .modelValue of type Date. It parses, formats and validates based
* on locale.
* *
* @customElement lion-input-date * @customElement lion-input-date
* @extends {LionInput} * @extends {LionInput}
*/ */
export class LionInputDate extends FieldCustomMixin(LocalizeMixin(LionInput)) { export class LionInputDate extends FieldCustomMixin(LocalizeMixin(LionInput)) {
static get asyncObservers() { static get properties() {
return { return {
...super.asyncObservers, modelValue: Date,
_calculateValues: ['locale'],
}; };
} }
@ -21,6 +21,14 @@ export class LionInputDate extends FieldCustomMixin(LocalizeMixin(LionInput)) {
super(); super();
this.parser = (value, options) => (value === '' ? undefined : parseDate(value, options)); this.parser = (value, options) => (value === '' ? undefined : parseDate(value, options));
this.formatter = formatDate; this.formatter = formatDate;
this.defaultValidators.push(new IsDate());
}
updated(c) {
super.updated(c);
if (c.has('locale')) {
this._calculateValues();
}
} }
connectedCallback() { connectedCallback() {
@ -28,11 +36,4 @@ export class LionInputDate extends FieldCustomMixin(LocalizeMixin(LionInput)) {
super.connectedCallback(); super.connectedCallback();
this.type = 'text'; this.type = 'text';
} }
getValidatorsForType(type) {
if (type === 'error') {
return [isDateValidator()].concat(super.getValidatorsForType(type) || []);
}
return super.getValidatorsForType(type);
}
} }

View file

@ -1,5 +1,6 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { maxDateValidator, minDateValidator, minMaxDateValidator } from '@lion/validate'; import { formatDate } from '@lion/localize';
import { MaxDate, MinDate, MinMaxDate } from '@lion/validate';
import '../lion-input-date.js'; import '../lion-input-date.js';
@ -11,38 +12,37 @@ storiesOf('Forms|Input Date', module)
`, `,
) )
.add( .add(
'minDateValidator', 'Validation',
() => html` () => html`
<lion-input-date label="IsDate" .modelValue=${new Date('foo')}> </lion-input-date>
<lion-input-date <lion-input-date
label="MinDate" label="MinDate"
help-text="Enter a date greater than or equal to today" help-text="Enter a date greater than or equal to today."
.errorValidators=${[minDateValidator(new Date())]} .modelValue=${new Date('2018/05/30')}
.validators=${[new MinDate(new Date())]}
> >
</lion-input-date> </lion-input-date>
`,
)
.add(
'maxDateValidator',
() => html`
<lion-input-date <lion-input-date
label="MaxDate" label="MaxDate"
help-text="Enter a date smaller than or equal to today" help-text="Enter a date smaller than or equal to today."
.errorValidators=${[maxDateValidator(new Date())]} .modelValue=${new Date('2100/05/30')}
.validators=${[new MaxDate(new Date())]}
> >
</lion-input-date> </lion-input-date>
`,
)
.add(
'minMaxDateValidator',
() => html`
<lion-input-date <lion-input-date
label="MinMaxDate" label="MinMaxDate"
help-text="Enter a date between '2018/05/24' and '2018/06/24'"
.modelValue=${new Date('2018/05/30')} .modelValue=${new Date('2018/05/30')}
.errorValidators=${[ .validators=${[
minMaxDateValidator({ min: new Date('2018/05/24'), max: new Date('2018/06/24') }), new MinMaxDate({ min: new Date('2018/05/24'), max: new Date('2018/06/24') }),
]} ]}
> >
<div slot="help-text">
Enter a date between ${formatDate(new Date('2018/05/24'))} and
${formatDate(new Date('2018/06/24'))}.
</div>
</lion-input-date> </lion-input-date>
`, `,
) )

View file

@ -3,7 +3,7 @@ import { html } from '@lion/core';
import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { localizeTearDown } from '@lion/localize/test-helpers.js';
import { localize } from '@lion/localize'; import { localize } from '@lion/localize';
import { maxDateValidator } from '@lion/validate'; import { MaxDate } from '@lion/validate';
import '../lion-input-date.js'; import '../lion-input-date.js';
@ -25,21 +25,31 @@ describe('<lion-input-date>', () => {
it('has validator "isDate" applied by default', async () => { it('has validator "isDate" applied by default', async () => {
const el = await fixture(`<lion-input-date></lion-input-date>`); const el = await fixture(`<lion-input-date></lion-input-date>`);
el.modelValue = '2005/11/10'; el.modelValue = '2005/11/10';
expect(el.errorState).to.equal(true); expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('IsDate');
el.modelValue = new Date('2005/11/10'); el.modelValue = new Date('2005/11/10');
expect(el.errorState).to.equal(false); expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('IsDate');
}); });
it('gets validated by "maxDate" correctly', async () => { it('gets validated by "MaxDate" correctly', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-input-date <lion-input-date
.modelValue=${new Date('2017/06/15')} .modelValue=${new Date('2017/06/15')}
.errorValidators=${[maxDateValidator(new Date('2017/06/14'))]} .validators=${[new MaxDate(new Date('2017/06/14'))]}
></lion-input-date> ></lion-input-date>
`); `);
expect(el.errorState).to.equal(true); expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('MaxDate');
el.modelValue = new Date('2017/06/14'); el.modelValue = new Date('2017/06/14');
expect(el.errorState).to.equal(false); expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('MaxDate');
}); });
it('uses formatOptions.locale', async () => { it('uses formatOptions.locale', async () => {

View file

@ -10,12 +10,12 @@ For an input field with a big range, such as `birthday-input`, a datepicker is n
- makes use of [lion-calendar](../calendar) inside the datepicker - makes use of [lion-calendar](../calendar) inside the datepicker
- makes use of [formatDate](../localize/docs/date.md) for formatting and parsing. - makes use of [formatDate](../localize/docs/date.md) for formatting and parsing.
- option to overwrite locale to change the formatting and parsing - option to overwrite locale to change the formatting and parsing
- can make use of date specific [validators](../validate/docs/DefaultValidators.md) with corresponding error messages in different languages - can make use of date specific [validators](../validate/docs/DefaultVaValidationSystemlidators.md) with corresponding error messages in different languages
- isDate (default) - IsDate (default)
- minDate - MinDate
- maxDate - MaxDate
- minMaxDate - MinMaxDate
- isDateDisabled - IsDateDisabled
## How to use ## How to use
@ -29,7 +29,7 @@ npm i --save @lion/input-datepicker
import '@lion/input-datepicker/lion-input-datepicker.js'; import '@lion/input-datepicker/lion-input-datepicker.js';
// validator import example // validator import example
import { minDateValidator } from '@lion/validate'; import { Required, MinDate } from '@lion/validate';
``` ```
### Example ### Example
@ -37,6 +37,6 @@ import { minDateValidator } from '@lion/validate';
```html ```html
<lion-input-datepicker <lion-input-datepicker
name="date" name="date"
.errorValidators="${[['required'], minDateValidator(new Date('2019/06/15'))]}" .validators="${[new Required(), new MinDate(new Date('2018/05/24'))]}"
></lion-input-datepicker> ></lion-input-datepicker>
``` ```

View file

@ -1,7 +1,7 @@
import { html, ifDefined, render } from '@lion/core'; import { html, ifDefined, render } from '@lion/core';
import { LionInputDate } from '@lion/input-date'; import { LionInputDate } from '@lion/input-date';
import { OverlayController, withModalDialogConfig, OverlayMixin } from '@lion/overlays'; import { OverlayController, withModalDialogConfig, OverlayMixin } from '@lion/overlays';
import { isValidatorApplied } from '@lion/validate';
import '@lion/calendar/lion-calendar.js'; import '@lion/calendar/lion-calendar.js';
import './lion-calendar-overlay-frame.js'; import './lion-calendar-overlay-frame.js';
@ -190,9 +190,8 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
*/ */
updated(c) { updated(c) {
super.updated(c); super.updated(c);
if (c.has('validators')) {
if (c.has('errorValidators') || c.has('warningValidators')) { const validators = [...(this.validators || [])];
const validators = [...(this.warningValidators || []), ...(this.errorValidators || [])];
this.__syncDisabledDates(validators); this.__syncDisabledDates(validators);
} }
if (c.has('label')) { if (c.has('label')) {
@ -314,18 +313,16 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
__syncDisabledDates(validators) { __syncDisabledDates(validators) {
// On every validator change, synchronize disabled dates: this means // On every validator change, synchronize disabled dates: this means
// we need to extract minDate, maxDate, minMaxDate and disabledDates validators // we need to extract minDate, maxDate, minMaxDate and disabledDates validators
validators.forEach(([fn, param]) => { validators.forEach(v => {
const d = new Date(); if (v.name === 'MinDate') {
this.__calendarMinDate = v.param;
if (isValidatorApplied('minDate', fn, d)) { } else if (v.name === 'MaxDate') {
this.__calendarMinDate = param; this.__calendarMaxDate = v.param;
} else if (isValidatorApplied('maxDate', fn, d)) { } else if (v.name === 'MinMaxDate') {
this.__calendarMaxDate = param; this.__calendarMinDate = v.param.min;
} else if (isValidatorApplied('minMaxDate', fn, { min: d, max: d })) { this.__calendarMaxDate = v.param.max;
this.__calendarMinDate = param.min; } else if (v.name === 'IsDateDisabled') {
this.__calendarMaxDate = param.max; this.__calendarDisableDates = v.param;
} else if (isValidatorApplied('isDateDisabled', fn, () => true)) {
this.__calendarDisableDates = param;
} }
}); });
} }

View file

@ -1,5 +1,6 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { isDateDisabledValidator, minMaxDateValidator } from '@lion/validate'; import { formatDate } from '@lion/localize';
import { IsDateDisabled, MinMaxDate } from '@lion/validate';
import '../lion-input-datepicker.js'; import '../lion-input-datepicker.js';
storiesOf('Forms|Input Datepicker', module) storiesOf('Forms|Input Datepicker', module)
@ -11,26 +12,25 @@ storiesOf('Forms|Input Datepicker', module)
`, `,
) )
.add( .add(
'minMaxDateValidator', 'Validation',
() => html` () => html`
<lion-input-datepicker <lion-input-datepicker
label="MinMaxDate" label="MinMaxDate"
help-text="Enter a date between '2018/05/24' and '2018/06/24'"
.modelValue=${new Date('2018/05/30')} .modelValue=${new Date('2018/05/30')}
.errorValidators=${[ .validators=${[
minMaxDateValidator({ min: new Date('2018/05/24'), max: new Date('2018/06/24') }), new MinMaxDate({ min: new Date('2018/05/24'), max: new Date('2018/06/24') }),
]} ]}
> >
<div slot="help-text">
Enter a date between ${formatDate(new Date('2018/05/24'))} and
${formatDate(new Date('2018/06/24'))}.
</div>
</lion-input-datepicker> </lion-input-datepicker>
`,
)
.add(
'isDateDisabledValidator',
() => html`
<lion-input-datepicker <lion-input-datepicker
label="isDateDisabled" label="IsDateDisabled"
help-text="You're not allowed to choose the 15th" help-text="You're not allowed to choose any 15th."
.errorValidators=${[isDateDisabledValidator(d => d.getDate() === 15)]} .validators=${[new IsDateDisabled(d => d.getDate() === 15)]}
> >
</lion-input-datepicker> </lion-input-datepicker>
`, `,

View file

@ -1,12 +1,7 @@
import { expect, fixture, defineCE } from '@open-wc/testing'; import { expect, fixture, defineCE } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import { html, LitElement } from '@lion/core'; import { html, LitElement } from '@lion/core';
import { import { MaxDate, MinDate, MinMaxDate, IsDateDisabled } from '@lion/validate';
maxDateValidator,
minDateValidator,
minMaxDateValidator,
isDateDisabledValidator,
} from '@lion/validate';
import { LionCalendar } from '@lion/calendar'; import { LionCalendar } from '@lion/calendar';
import { isSameDate } from '@lion/calendar/src/utils/isSameDate.js'; import { isSameDate } from '@lion/calendar/src/utils/isSameDate.js';
import { DatepickerInputObject } from '../test-helpers.js'; import { DatepickerInputObject } from '../test-helpers.js';
@ -196,12 +191,12 @@ describe('<lion-input-datepicker>', () => {
* - all validators will be translated under the hood to enabledDates and passed to * - all validators will be translated under the hood to enabledDates and passed to
* lion-calendar * lion-calendar
*/ */
it('converts isDateDisabledValidator to "disableDates" property', async () => { it('converts IsDateDisabled validator to "disableDates" property', async () => {
const no15th = d => d.getDate() !== 15; const no15th = d => d.getDate() !== 15;
const no16th = d => d.getDate() !== 16; const no16th = d => d.getDate() !== 16;
const no15thOr16th = d => no15th(d) && no16th(d); const no15thOr16th = d => no15th(d) && no16th(d);
const el = await fixture(html` const el = await fixture(html`
<lion-input-datepicker .errorValidators="${[isDateDisabledValidator(no15thOr16th)]}"> <lion-input-datepicker .validators="${[new IsDateDisabled(no15thOr16th)]}">
</lion-input-datepicker> </lion-input-datepicker>
`); `);
const elObj = new DatepickerInputObject(el); const elObj = new DatepickerInputObject(el);
@ -210,10 +205,10 @@ describe('<lion-input-datepicker>', () => {
expect(elObj.calendarEl.disableDates).to.equal(no15thOr16th); expect(elObj.calendarEl.disableDates).to.equal(no15thOr16th);
}); });
it('converts minDateValidator to "minDate" property', async () => { it('converts MinDate validator to "minDate" property', async () => {
const myMinDate = new Date('2019/06/15'); const myMinDate = new Date('2019/06/15');
const el = await fixture(html` const el = await fixture(html`
<lion-input-datepicker .errorValidators=${[minDateValidator(myMinDate)]}> <lion-input-datepicker .validators="${[new MinDate(myMinDate)]}">
</lion-input-date>`); </lion-input-date>`);
const elObj = new DatepickerInputObject(el); const elObj = new DatepickerInputObject(el);
await elObj.openCalendar(); await elObj.openCalendar();
@ -221,11 +216,10 @@ describe('<lion-input-datepicker>', () => {
expect(elObj.calendarEl.minDate).to.equal(myMinDate); expect(elObj.calendarEl.minDate).to.equal(myMinDate);
}); });
it('converts maxDateValidator to "maxDate" property', async () => { it('converts MaxDate validator to "maxDate" property', async () => {
const myMaxDate = new Date('2030/06/15'); const myMaxDate = new Date('2030/06/15');
const el = await fixture(html` const el = await fixture(html`
<lion-input-datepicker .errorValidators=${[maxDateValidator(myMaxDate)]}> <lion-input-datepicker .validators=${[new MaxDate(myMaxDate)]}> </lion-input-datepicker>
</lion-input-datepicker>
`); `);
const elObj = new DatepickerInputObject(el); const elObj = new DatepickerInputObject(el);
await elObj.openCalendar(); await elObj.openCalendar();
@ -233,12 +227,12 @@ describe('<lion-input-datepicker>', () => {
expect(elObj.calendarEl.maxDate).to.equal(myMaxDate); expect(elObj.calendarEl.maxDate).to.equal(myMaxDate);
}); });
it('converts minMaxDateValidator to "minDate" and "maxDate" property', async () => { it('converts MinMaxDate validator to "minDate" and "maxDate" property', async () => {
const myMinDate = new Date('2019/06/15'); const myMinDate = new Date('2019/06/15');
const myMaxDate = new Date('2030/06/15'); const myMaxDate = new Date('2030/06/15');
const el = await fixture(html` const el = await fixture(html`
<lion-input-datepicker <lion-input-datepicker
.errorValidators=${[minMaxDateValidator({ min: myMinDate, max: myMaxDate })]} .validators=${[new MinMaxDate({ min: myMinDate, max: myMaxDate })]}
> >
</lion-input-datepicker> </lion-input-datepicker>
`); `);

View file

@ -7,9 +7,8 @@
## Features ## Features
- based on [lion-input](../input) - based on [lion-input](../input)
- default label in different languages - makes use of email [validators](../validate/docs/ValidationSystem.md) with corresponding error messages in different languages
- makes use of email [validators](../validate/docs/DefaultValidators.md) with corresponding error messages in different languages - IsEmail (default)
- isEmail (default)
## How to use ## How to use
@ -21,10 +20,13 @@ npm i --save @lion/input-email
```js ```js
import '@lion/input-email/lion-input-email.js'; import '@lion/input-email/lion-input-email.js';
// validator import example
import { Required } from '@lion/validate';
``` ```
### Example ### Example
```html ```html
<lion-input-email name="email" .errorValidators="${[['required']]}"></lion-input-email> <lion-input-email label="email" name="email" .validators="${['new Required()]}"></lion-input-email>
``` ```

View file

@ -1,7 +1,7 @@
import { LocalizeMixin } from '@lion/localize'; import { LocalizeMixin } from '@lion/localize';
import { FieldCustomMixin } from '@lion/field'; import { FieldCustomMixin } from '@lion/field';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import { isEmailValidator } from '@lion/validate'; import { IsEmail } from '@lion/validate';
/** /**
* LionInputEmail: extension of lion-input * LionInputEmail: extension of lion-input
@ -10,16 +10,14 @@ import { isEmailValidator } from '@lion/validate';
* @extends {LionInput} * @extends {LionInput}
*/ */
export class LionInputEmail extends FieldCustomMixin(LocalizeMixin(LionInput)) { export class LionInputEmail extends FieldCustomMixin(LocalizeMixin(LionInput)) {
getValidatorsForType(type) { constructor() {
if (type === 'error') { super();
// local-part@domain where the local part may be up to 64 characters long // local-part@domain where the local part may be up to 64 characters long
// and the domain may have a maximum of 255 characters // and the domain may have a maximum of 255 characters
// @see https://www.wikiwand.com/en/Email_address // @see https://www.wikiwand.com/en/Email_address
// however, the longest active email is even bigger // however, the longest active email is even bigger
// @see https://laughingsquid.com/the-worlds-longest-active-email-address/ // @see https://laughingsquid.com/the-worlds-longest-active-email-address/
// we don't want to forbid Mr. Peter Craig email right? // we don't want to forbid Mr. Peter Craig email right?
return [isEmailValidator()].concat(super.getValidatorsForType(type) || []); this.defaultValidators.push(new IsEmail());
}
return super.getValidatorsForType(type);
} }
} }

View file

@ -1,6 +1,5 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { Validator } from '@lion/validate';
import { localize } from '@lion/localize';
import '../lion-input-email.js'; import '../lion-input-email.js';
import '../../fieldset/lion-fieldset.js'; import '../../fieldset/lion-fieldset.js';
@ -15,33 +14,34 @@ storiesOf('Forms|Input Email', module)
.add( .add(
'Faulty prefilled', 'Faulty prefilled',
() => html` () => html`
<lion-input-email .modelValue=${'foo'} label="Label"></lion-input-email> <lion-input-email .modelValue=${'foo'} label="Email"></lion-input-email>
`, `,
) )
.add('Use own validator', () => { .add('Custom validator', () => {
const gmailOnly = modelValue => ({ gmailOnly: modelValue.indexOf('gmail.com') !== -1 }); class GmailOnly extends Validator {
localize.locale = 'en-GB'; constructor(...args) {
super(...args);
this.name = 'GmailOnly';
}
try { execute(value) {
localize.addData('en', 'lion-validate+gmailOnly', { let hasError = false;
error: { if (!(value.indexOf('gmail.com') !== -1)) {
gmailOnly: 'You can only use gmail.com email addresses.', hasError = true;
}, }
}); return hasError;
localize.addData('nl', 'lion-validate+gmailOnly', { }
error: {
gmailOnly: 'Je mag hier alleen gmail.com e-mailadressen gebruiken.', static async getMessage() {
}, return 'You can only use gmail.com email addresses.';
}); }
} catch (error) {
// expected as it's a demo
} }
return html` return html`
<lion-input-email <lion-input-email
.modelValue=${'foo@bar.com'} .modelValue=${'foo@bar.com'}
.errorValidators=${[[gmailOnly]]} .validators=${[new GmailOnly()]}
label="Label" label="Email"
></lion-input-email> ></lion-input-email>
`; `;
}); });

View file

@ -4,14 +4,15 @@ import '../lion-input-email.js';
describe('<lion-input-email>', () => { describe('<lion-input-email>', () => {
it('has a type = text', async () => { it('has a type = text', async () => {
const lionInputEmail = await fixture(`<lion-input-email></lion-input-email>`); const el = await fixture(`<lion-input-email></lion-input-email>`);
expect(lionInputEmail._inputNode.type).to.equal('text'); expect(el._inputNode.type).to.equal('text');
}); });
it('has validator "isEmail" applied by default', async () => { it('has validator "IsEmail" applied by default', async () => {
// More eloborate tests can be found in lion-validate/validators.js // More elaborate tests can be found in lion-validate/test/StringValidators.test.js
const lionInputEmail = await fixture(`<lion-input-email></lion-input-email>`); const el = await fixture(`<lion-input-email></lion-input-email>`);
lionInputEmail.modelValue = 'foo@bar@example.com'; el.modelValue = 'foo@bar@example.com';
expect(lionInputEmail.errorState).to.equal(true); expect(el.hasFeedbackFor).to.deep.equal(['error']);
expect(el.validationStates.error.IsEmail).to.be.true;
}); });
}); });

View file

@ -9,8 +9,8 @@
- based on [lion-input](../input) - based on [lion-input](../input)
- default label in different languages - default label in different languages
- makes use of IBAN specific [validate](../validate) with corresponding error messages in different languages - makes use of IBAN specific [validate](../validate) with corresponding error messages in different languages
- isIBAN (default) - IsIBAN (default)
- isCountryIBAN - IsCountryIBAN
## How to use ## How to use
@ -24,7 +24,7 @@ npm i --save @lion/input-amount
import '@lion/input-amount/lion-input-amount.js'; import '@lion/input-amount/lion-input-amount.js';
// validator import example // validator import example
import { isCountryIBANValidator } from '@lion/validate'; import { Required, IsCountryIBAN } from '@lion/validate';
``` ```
### Example ### Example
@ -32,6 +32,6 @@ import { isCountryIBANValidator } from '@lion/validate';
```html ```html
<lion-input-iban <lion-input-iban
name="account" name="account"
.errorValidators="${[['required'], isCountryIBANValidator('BE')]}" .validators="${[new Required(), new IsCountryIBAN('BE')]}"
></lion-input-iban> ></lion-input-iban>
``` ```

View file

@ -1,9 +1,4 @@
export { LionInputIban } from './src/LionInputIban.js'; export { LionInputIban } from './src/LionInputIban.js';
export { formatIBAN } from './src/formatters.js'; export { formatIBAN } from './src/formatters.js';
export { parseIBAN } from './src/parsers.js'; export { parseIBAN } from './src/parsers.js';
export { export { IsIBAN, IsCountryIBAN } from './src/validators.js';
isCountryIBAN,
isCountryIBANValidator,
isIBAN,
isIBANValidator,
} from './src/validators.js';

View file

@ -36,10 +36,10 @@
"@lion/core": "^0.3.0", "@lion/core": "^0.3.0",
"@lion/field": "^0.4.1", "@lion/field": "^0.4.1",
"@lion/input": "^0.2.1", "@lion/input": "^0.2.1",
"@lion/localize": "^0.5.0" "@lion/localize": "^0.5.0",
"@lion/validate": "^0.3.1"
}, },
"devDependencies": { "devDependencies": {
"@lion/validate": "^0.3.1",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4" "@open-wc/testing": "^2.3.4"
} }

View file

@ -3,7 +3,7 @@ import { LionInput } from '@lion/input';
import { FieldCustomMixin } from '@lion/field'; import { FieldCustomMixin } from '@lion/field';
import { formatIBAN } from './formatters.js'; import { formatIBAN } from './formatters.js';
import { parseIBAN } from './parsers.js'; import { parseIBAN } from './parsers.js';
import { isIBANValidator } from './validators.js'; import { IsIBAN } from './validators.js';
/** /**
* `LionInputIban` is a class for an IBAN custom form element (`<lion-input-iban>`). * `LionInputIban` is a class for an IBAN custom form element (`<lion-input-iban>`).
@ -11,99 +11,10 @@ import { isIBANValidator } from './validators.js';
* @extends {LionInput} * @extends {LionInput}
*/ */
export class LionInputIban extends FieldCustomMixin(LocalizeMixin(LionInput)) { export class LionInputIban extends FieldCustomMixin(LocalizeMixin(LionInput)) {
static get localizeNamespaces() {
return [
{
/* FIXME: This awful switch statement is used to make sure it works with polymer build.. */
'lion-input-iban': locale => {
switch (locale) {
case 'bg-BG':
return import('../translations/bg-BG.js');
case 'bg':
return import('../translations/bg.js');
case 'cs-CZ':
return import('../translations/cs-CZ.js');
case 'cs':
return import('../translations/cs.js');
case 'de-DE':
return import('../translations/de-DE.js');
case 'de':
return import('../translations/de.js');
case 'en-AU':
return import('../translations/en-AU.js');
case 'en-GB':
return import('../translations/en-GB.js');
case 'en-US':
return import('../translations/en-US.js');
case 'en-PH':
case 'en':
return import('../translations/en.js');
case 'es-ES':
return import('../translations/es-ES.js');
case 'es':
return import('../translations/es.js');
case 'fr-FR':
return import('../translations/fr-FR.js');
case 'fr-BE':
return import('../translations/fr-BE.js');
case 'fr':
return import('../translations/fr.js');
case 'hu-HU':
return import('../translations/hu-HU.js');
case 'hu':
return import('../translations/hu.js');
case 'it-IT':
return import('../translations/it-IT.js');
case 'it':
return import('../translations/it.js');
case 'nl-BE':
return import('../translations/nl-BE.js');
case 'nl-NL':
return import('../translations/nl-NL.js');
case 'nl':
return import('../translations/nl.js');
case 'pl-PL':
return import('../translations/pl-PL.js');
case 'pl':
return import('../translations/pl.js');
case 'ro-RO':
return import('../translations/ro-RO.js');
case 'ro':
return import('../translations/ro.js');
case 'ru-RU':
return import('../translations/ru-RU.js');
case 'ru':
return import('../translations/ru.js');
case 'sk-SK':
return import('../translations/sk-SK.js');
case 'sk':
return import('../translations/sk.js');
case 'uk-UA':
return import('../translations/uk-UA.js');
case 'uk':
return import('../translations/uk.js');
case 'zh-CN':
case 'zh':
return import('../translations/zh.js');
default:
return import(`../translations/${locale}.js`);
}
},
},
...super.localizeNamespaces,
];
}
constructor() { constructor() {
super(); super();
this.formatter = formatIBAN; this.formatter = formatIBAN;
this.parser = parseIBAN; this.parser = parseIBAN;
} this.defaultValidators.push(new IsIBAN());
getValidatorsForType(type) {
if (type === 'error') {
return [isIBANValidator()].concat(super.getValidatorsForType(type) || []);
}
return super.getValidatorsForType(type);
} }
} }

View file

@ -1,12 +1,59 @@
/* eslint-disable max-classes-per-file */
import { isValidIBAN } from '@bundled-es-modules/ibantools/ibantools.js'; import { isValidIBAN } from '@bundled-es-modules/ibantools/ibantools.js';
import { Validator } from '@lion/validate';
import { localize } from '@lion/localize';
export const isIBAN = value => isValidIBAN(value); let loaded = false;
const loadTranslations = async () => {
if (loaded) {
return;
}
await localize.loadNamespace(
{
'lion-validate+iban': locale => import(`../translations/${locale}.js`),
},
{ locale: localize.localize },
);
loaded = true;
};
export const isIBANValidator = () => [(...params) => ({ isIBAN: isIBAN(...params) })]; export class IsIBAN extends Validator {
constructor(...args) {
super(...args);
this.name = 'IsIBAN';
}
export const isCountryIBAN = (value, country = '') => // eslint-disable-next-line class-methods-use-this
isIBAN(value) && value.slice(0, 2) === country; execute(value) {
export const isCountryIBANValidator = (...factoryParams) => [ return !isValidIBAN(value);
(...params) => ({ isCountryIBAN: isCountryIBAN(...params) }), }
...factoryParams,
]; static async getMessage(data) {
await loadTranslations();
return localize.msg('lion-validate+iban:error.IsIBAN', data);
}
}
export class IsCountryIBAN extends IsIBAN {
constructor(...args) {
super(...args);
this.name = 'IsCountryIBAN';
}
execute(value) {
const notIBAN = super.execute(value);
if (value.slice(0, 2) !== this.param) {
return true;
}
if (notIBAN) {
return true;
}
return false;
}
static async getMessage(data) {
await loadTranslations();
return localize.msg('lion-validate+iban:error.IsCountryIBAN', data);
}
}

View file

@ -1,13 +1,13 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { isCountryIBANValidator } from '../index.js'; import { IsCountryIBAN } from '../index.js';
import '../lion-input-iban.js'; import '../lion-input-iban.js';
storiesOf('Forms|Input IBAN', module) storiesOf('Forms|Input IBAN', module)
.add( .add(
'Default', 'Default',
() => html` () => html`
<lion-input-iban name="iban" label="Label"></lion-input-iban> <lion-input-iban name="iban" label="IBAN"></lion-input-iban>
`, `,
) )
.add( .add(
@ -16,7 +16,7 @@ storiesOf('Forms|Input IBAN', module)
<lion-input-iban <lion-input-iban
.modelValue=${'NL20INGB0001234567'} .modelValue=${'NL20INGB0001234567'}
name="iban" name="iban"
label="Label" label="IBAN"
></lion-input-iban> ></lion-input-iban>
`, `,
) )
@ -26,7 +26,7 @@ storiesOf('Forms|Input IBAN', module)
<lion-input-iban <lion-input-iban
.modelValue=${'NL20INGB0001234567XXXX'} .modelValue=${'NL20INGB0001234567XXXX'}
name="iban" name="iban"
label="Label" label="IBAN"
></lion-input-iban> ></lion-input-iban>
`, `,
) )
@ -35,9 +35,9 @@ storiesOf('Forms|Input IBAN', module)
() => html` () => html`
<lion-input-iban <lion-input-iban
.modelValue=${'DE89370400440532013000'} .modelValue=${'DE89370400440532013000'}
.errorValidators=${[isCountryIBANValidator('NL')]} .validators=${[new IsCountryIBAN('NL')]}
name="iban" name="iban"
label="Label" label="IBAN"
></lion-input-iban> ></lion-input-iban>
`, `,
); );

View file

@ -1,7 +1,7 @@
import { expect, fixture } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from '@lion/core'; import { html } from '@lion/core';
import { isCountryIBANValidator } from '../src/validators.js'; import { IsCountryIBAN } from '../src/validators.js';
import { formatIBAN } from '../src/formatters.js'; import { formatIBAN } from '../src/formatters.js';
import { parseIBAN } from '../src/parsers.js'; import { parseIBAN } from '../src/parsers.js';
@ -23,25 +23,34 @@ describe('<lion-input-iban>', () => {
expect(el._inputNode.type).to.equal('text'); expect(el._inputNode.type).to.equal('text');
}); });
it('has validator "isIBAN" applied by default', async () => { it('has validator "IsIBAN" applied by default', async () => {
const el = await fixture(`<lion-input-iban></lion-input-iban>`); const el = await fixture(`<lion-input-iban></lion-input-iban>`);
el.modelValue = 'FOO'; el.modelValue = 'FOO';
expect(el.error.isIBAN).to.be.true; expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('IsIBAN');
el.modelValue = 'DE89370400440532013000'; el.modelValue = 'DE89370400440532013000';
expect(el.error.isIBAN).to.be.undefined; expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('IsIBAN');
}); });
it('can apply validator "isCountryIBAN" to restrict countries', async () => { it('can apply validator "IsCountryIBAN" to restrict countries', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-input-iban .errorValidators=${[isCountryIBANValidator('NL')]}></lion-input-iban> <lion-input-iban .validators=${[new IsCountryIBAN('NL')]}> </lion-input-iban>
`); `);
el.modelValue = 'DE89370400440532013000'; el.modelValue = 'DE89370400440532013000';
expect(el.error.isCountryIBAN).to.be.true; expect(el.hasFeedbackFor).to.include('error');
expect(el.error.isIBAN).to.be.undefined; expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('IsCountryIBAN');
el.modelValue = 'NL17INGB0002822608'; el.modelValue = 'NL17INGB0002822608';
expect(el.error.isCountryIBAN).to.be.undefined; expect(el.hasFeedbackFor).not.to.include('error');
expect(el.error.isIBAN).to.be.undefined; expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('IsCountryIBAN');
el.modelValue = 'FOO'; el.modelValue = 'FOO';
expect(el.error.isIBAN).to.be.true; expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('IsIBAN');
expect(el.validationStates.error).to.have.a.property('IsCountryIBAN');
}); });
}); });

View file

@ -1,24 +1,22 @@
import { expect } from '@open-wc/testing'; import { expect } from '@open-wc/testing';
import { smokeTestValidator } from '@lion/validate/test-helpers.js';
import { import { IsIBAN, IsCountryIBAN } from '../src/validators.js';
isIBAN,
isIBANValidator, import '../lion-input-iban.js';
isCountryIBAN,
isCountryIBANValidator,
} from '../src/validators.js';
describe('IBAN validation', () => { describe('IBAN validation', () => {
it('provides isIBAN() to check for valid IBAN', () => { it('provides IsIBAN to check for valid IBAN', () => {
expect(isIBAN('NL17INGB0002822608')).to.be.true; const validator = new IsIBAN();
expect(isIBAN('DE89370400440532013000')).to.be.true; expect(validator.execute('NL17INGB0002822608')).to.be.false;
smokeTestValidator('isIBAN', isIBANValidator, 'NL17INGB0002822608'); expect(validator.execute('DE89370400440532013000')).to.be.false;
}); });
it('provides isCountryIBAN() to limit IBANs from specfic countries', () => {
expect(isCountryIBAN('NL17INGB0002822608', 'NL')).to.be.true; it('provides IsCountryIBAN to limit IBANs from specific countries', () => {
expect(isCountryIBAN('DE89370400440532013000', 'DE')).to.be.true; const nlValidator = new IsCountryIBAN('NL');
expect(isCountryIBAN('DE89370400440532013000', 'NL')).to.be.false; const deValidator = new IsCountryIBAN('DE');
expect(isCountryIBAN('foo', 'NL')).to.be.false; expect(nlValidator.execute('NL17INGB0002822608')).to.be.false;
smokeTestValidator('isCountryIBAN', isCountryIBANValidator, 'NL17INGB0002822608', 'NL'); expect(deValidator.execute('DE89370400440532013000')).to.be.false;
expect(nlValidator.execute('DE89370400440532013000')).to.be.true;
expect(nlValidator.execute('foo')).to.be.true;
}); });
}); });

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Введіть правильні дані {fieldName}.', IsIBAN: 'Введіть правильні дані {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Моля, въведете валиден {validatorParams, select,\n' + 'Моля, въведете валиден {params, select,\n' +
'AT {Австрийски}\n' + 'AT {Австрийски}\n' +
'BE {Белгийски}\n' + 'BE {Белгийски}\n' +
'CZ {Чешки}\n' + 'CZ {Чешки}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Нидерландски}\n' + 'NL {Нидерландски}\n' +
'PL {Полски}\n' + 'PL {Полски}\n' +
'RO {Румънски}\n' + 'RO {Румънски}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName}.', '} {fieldName}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Zadejte platné {fieldName}.', IsIBAN: 'Zadejte platné {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Zadejte platnou {validatorParams, select,\n' + 'Zadejte platnou {params, select,\n' +
'AT {Rakušan}\n' + 'AT {Rakušan}\n' +
'BE {Belgičan}\n' + 'BE {Belgičan}\n' +
'CZ {Čech}\n' + 'CZ {Čech}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Holanďan}\n' + 'NL {Holanďan}\n' +
'PL {Polák}\n' + 'PL {Polák}\n' +
'RO {Rumun}\n' + 'RO {Rumun}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName}.', '} {fieldName}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Geben Sie ein gültiges {fieldName} ein.', IsIBAN: 'Geben Sie ein gültiges {fieldName} ein.',
isCountryIBAN: IsCountryIBAN:
'Geben Sie eine gültige {validatorParams, select,\n' + 'Geben Sie eine gültige {params, select,\n' +
'AT {Österreichisch}\n' + 'AT {Österreichisch}\n' +
'BE {Belgisch}\n' + 'BE {Belgisch}\n' +
'CZ {Tschechisch}\n' + 'CZ {Tschechisch}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Niederländisch}\n' + 'NL {Niederländisch}\n' +
'PL {Polnisch}\n' + 'PL {Polnisch}\n' +
'RO {Rumänisch}\n' + 'RO {Rumänisch}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName} ein.', '} {fieldName} ein.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Please enter a valid {fieldName}.', IsIBAN: 'Please enter a valid {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Please enter a valid {validatorParams, select,\n' + 'Please enter a valid {params, select,\n' +
'AT {Austrian}\n' + 'AT {Austrian}\n' +
'BE {Belgian}\n' + 'BE {Belgian}\n' +
'CZ {Czech}\n' + 'CZ {Czech}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Dutch}\n' + 'NL {Dutch}\n' +
'PL {Polish}\n' + 'PL {Polish}\n' +
'RO {Romanian}\n' + 'RO {Romanian}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName}.', '} {fieldName}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Introduzca un/a {fieldName} válido/a.', IsIBAN: 'Introduzca un/a {fieldName} válido/a.',
isCountryIBAN: IsCountryIBAN:
'Introduzca un/a {fieldName} válido/a de {validatorParams, select,\n' + 'Introduzca un/a {fieldName} válido/a de {params, select,\n' +
'AT {Austriaco}\n' + 'AT {Austriaco}\n' +
'BE {Belga}\n' + 'BE {Belga}\n' +
'CZ {Checo}\n' + 'CZ {Checo}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Neerlandés}\n' + 'NL {Neerlandés}\n' +
'PL {Polaco}\n' + 'PL {Polaco}\n' +
'RO {Rumano}\n' + 'RO {Rumano}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'}.', '}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Indiquez un(e) {fieldName} valide.', IsIBAN: 'Indiquez un(e) {fieldName} valide.',
isCountryIBAN: IsCountryIBAN:
'Veuillez saisir un(e) {fieldName} {validatorParams, select,\n' + 'Veuillez saisir un(e) {fieldName} {params, select,\n' +
'AT {autrichien}\n' + 'AT {autrichien}\n' +
'BE {belge}\n' + 'BE {belge}\n' +
'CZ {tchèque}\n' + 'CZ {tchèque}\n' +
@ -14,7 +14,7 @@ export default {
'NL {néerlandais}\n' + 'NL {néerlandais}\n' +
'PL {polonais}\n' + 'PL {polonais}\n' +
'RO {roumain}\n' + 'RO {roumain}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} valide.', '} valide.',
}, },
}; };

View file

@ -1,5 +1,5 @@
import bg from './bg.js'; import hu from './hu.js';
export default { export default {
...bg, ...hu,
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Kérjük, adjon meg érvényes {fieldName} értéket.', IsIBAN: 'Kérjük, adjon meg érvényes {fieldName} értéket.',
isCountryIBAN: IsCountryIBAN:
'Kérjük, adjon meg érvényes {validatorParams, select,\n' + 'Kérjük, adjon meg érvényes {params, select,\n' +
'AT {Osztrák}\n' + 'AT {Osztrák}\n' +
'BE {Belga}\n' + 'BE {Belga}\n' +
'CZ {Cseh}\n' + 'CZ {Cseh}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Holland}\n' + 'NL {Holland}\n' +
'PL {Lengyel}\n' + 'PL {Lengyel}\n' +
'RO {Román}\n' + 'RO {Román}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName} értéket.', '} {fieldName} értéket.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Inserire un valore valido per {fieldName}.', IsIBAN: 'Inserire un valore valido per {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Inserire un valore valido per {fieldName} {validatorParams, select,\n' + 'Inserire un valore valido per {fieldName} {params, select,\n' +
'AT {Austriaco}\n' + 'AT {Austriaco}\n' +
'BE {Belga}\n' + 'BE {Belga}\n' +
'CZ {Ceco}\n' + 'CZ {Ceco}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Olandese}\n' + 'NL {Olandese}\n' +
'PL {Polacco}\n' + 'PL {Polacco}\n' +
'RO {Rumeno}\n' + 'RO {Rumeno}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'}.', '}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Vul een geldig(e) {fieldName} in.', IsIBAN: 'Vul een geldig(e) {fieldName} in.',
isCountryIBAN: IsCountryIBAN:
'Vul een geldig(e) {validatorParams, select,\n' + 'Vul een geldig(e) {params, select,\n' +
'AT {Oostenrijkse}\n' + 'AT {Oostenrijkse}\n' +
'BE {Belgische}\n' + 'BE {Belgische}\n' +
'CZ {Tsjechische}\n' + 'CZ {Tsjechische}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Nederlandse}\n' + 'NL {Nederlandse}\n' +
'PL {Poolse}\n' + 'PL {Poolse}\n' +
'RO {Roemeense}\n' + 'RO {Roemeense}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName} in.', '} {fieldName} in.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Wprowadź prawidłową wartość w polu {fieldName}.', IsIBAN: 'Wprowadź prawidłową wartość w polu {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Wprowadź prawidłową wartość w polu {validatorParams, select,\n' + 'Wprowadź prawidłową wartość w polu {params, select,\n' +
'AT {Austriacki}\n' + 'AT {Austriacki}\n' +
'BE {Belgijski}\n' + 'BE {Belgijski}\n' +
'CZ {Czeski}\n' + 'CZ {Czeski}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Holenderski}\n' + 'NL {Holenderski}\n' +
'PL {Polski}\n' + 'PL {Polski}\n' +
'RO {Rumuński}\n' + 'RO {Rumuński}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName}.', '} {fieldName}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Vă rugăm să introduceți un/o {fieldName} valid(ă).', IsIBAN: 'Vă rugăm să introduceți un/o {fieldName} valid(ă).',
isCountryIBAN: IsCountryIBAN:
'Vă rugăm să introduceți un/o {fieldName} {validatorParams, select,\n' + 'Vă rugăm să introduceți un/o {fieldName} {params, select,\n' +
'AT {austriac}\n' + 'AT {austriac}\n' +
'BE {belgian}\n' + 'BE {belgian}\n' +
'CZ {ceh}\n' + 'CZ {ceh}\n' +
@ -14,7 +14,7 @@ export default {
'NL {olandez}\n' + 'NL {olandez}\n' +
'PL {polonez}\n' + 'PL {polonez}\n' +
'RO {românesc}\n' + 'RO {românesc}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} valid(ă).', '} valid(ă).',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Введите действительное значение поля {fieldName}.', IsIBAN: 'Введите действительное значение поля {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Введите действительное значение поля {validatorParams, select,\n' + 'Введите действительное значение поля {params, select,\n' +
'AT {Австрийский}\n' + 'AT {Австрийский}\n' +
'BE {Бельгийский}\n' + 'BE {Бельгийский}\n' +
'CZ {Чешский}\n' + 'CZ {Чешский}\n' +
@ -14,7 +14,7 @@ export default {
'NL {Нидерландский}\n' + 'NL {Нидерландский}\n' +
'PL {Польский}\n' + 'PL {Польский}\n' +
'RO {Румынский}\n' + 'RO {Румынский}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName}.', '} {fieldName}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Zadajte platnú hodnotu do poľa {fieldName}.', IsIBAN: 'Zadajte platnú hodnotu do poľa {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Zadajte platný {validatorParams, select,\n' + 'Zadajte platný {params, select,\n' +
'AT {rakúsky}\n' + 'AT {rakúsky}\n' +
'BE {belgický}\n' + 'BE {belgický}\n' +
'CZ {český}\n' + 'CZ {český}\n' +
@ -14,7 +14,7 @@ export default {
'NL {holandský}\n' + 'NL {holandský}\n' +
'PL {poľský}\n' + 'PL {poľský}\n' +
'RO {rumunský}\n' + 'RO {rumunský}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} kód {fieldName}.', '} kód {fieldName}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: 'Введіть правильні дані {fieldName}.', IsIBAN: 'Введіть правильні дані {fieldName}.',
isCountryIBAN: IsCountryIBAN:
'Введіть правильні дані {validatorParams, select,\n' + 'Введіть правильні дані {params, select,\n' +
'AT {австрійський}\n' + 'AT {австрійський}\n' +
'BE {бельгійський}\n' + 'BE {бельгійський}\n' +
'CZ {чеський}\n' + 'CZ {чеський}\n' +
@ -14,7 +14,7 @@ export default {
'NL {голландський}\n' + 'NL {голландський}\n' +
'PL {польський}\n' + 'PL {польський}\n' +
'RO {румунська}\n' + 'RO {румунська}\n' +
'other {{validatorParams}}\n' + 'other {{params}}\n' +
'} {fieldName}.', '} {fieldName}.',
}, },
}; };

View file

@ -1,8 +1,8 @@
export default { export default {
error: { error: {
isIBAN: '請輸入有效的{fieldName}。', IsIBAN: '請輸入有效的{fieldName}。',
isCountryIBAN: IsCountryIBAN:
'請輸入有效的{validatorParams, select,\n' + '請輸入有效的{params, select,\n' +
'AT {奥}\n' + 'AT {奥}\n' +
'BE {比利时的}\n' + 'BE {比利时的}\n' +
'CZ {捷克}\n' + 'CZ {捷克}\n' +
@ -14,7 +14,7 @@ export default {
'NL {荷兰人}\n' + 'NL {荷兰人}\n' +
'PL {抛光}\n' + 'PL {抛光}\n' +
'RO {罗马尼亚}\n' + 'RO {罗马尼亚}\n' +
'另一个 {{validatorParams}}\n' + '另一个 {{params}}\n' +
'} {fieldName}。', '} {fieldName}。',
}, },
}; };

View file

@ -1,8 +1,10 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { maxLengthValidator } from '@lion/validate'; import { MaxLength, Validator, loadDefaultFeedbackMessages } from '@lion/validate';
import { localize, LocalizeMixin } from '@lion/localize'; import { localize, LocalizeMixin } from '@lion/localize';
import { LionInput } from '../index.js'; import { LionInput } from '../index.js';
loadDefaultFeedbackMessages();
storiesOf('Forms|Input Localize', module).add('localize', () => { storiesOf('Forms|Input Localize', module).add('localize', () => {
class InputValidationExample extends LocalizeMixin(LionInput) { class InputValidationExample extends LocalizeMixin(LionInput) {
static get localizeNamespaces() { static get localizeNamespaces() {
@ -23,19 +25,29 @@ storiesOf('Forms|Input Localize', module).add('localize', () => {
customElements.define('input-localize-example', InputValidationExample); customElements.define('input-localize-example', InputValidationExample);
} }
const notEqualsString = (value, stringValue) => stringValue.toString() !== value; class NotEqualsString extends Validator {
const notEqualsStringValidator = (...factoryParams) => [ constructor(...args) {
(...params) => ({ notEqualsString: notEqualsString(...params) }), super(...args);
factoryParams, this.name = 'NotEqualsString';
]; }
execute(value, param) {
const hasError = value === param;
return hasError;
}
static async getMessage() {
return localize.msg(`input-localize-example:error.notEqualsString`);
}
}
return html` return html`
<input-localize-example <input-localize-example
.errorValidators=${[maxLengthValidator(5)]} .validators=${[new MaxLength(5)]}
.modelValue=${'default validator'} .modelValue=${'default validator'}
></input-localize-example> ></input-localize-example>
<input-localize-example <input-localize-example
.errorValidators=${[notEqualsStringValidator('custom validator')]} .validators=${[new NotEqualsString('custom validator')]}
.modelValue=${'custom validator'} .modelValue=${'custom validator'}
></input-localize-example> ></input-localize-example>
<p> <p>

View file

@ -1,131 +0,0 @@
import { storiesOf, html } from '@open-wc/demoing-storybook';
import {
equalsLengthValidator,
minLengthValidator,
maxLengthValidator,
minMaxLengthValidator,
isEmailValidator,
} from '@lion/validate';
import { LocalizeMixin } from '@lion/localize';
import { LionInput } from '../index.js';
storiesOf('Forms|Input String Validation', module)
.add(
'equalsLength',
() => html`
<lion-input
.errorValidators=${[equalsLengthValidator(7)]}
.modelValue=${'not exactly'}
label="equalsLengthValidator"
></lion-input>
<lion-input
.errorValidators=${[equalsLengthValidator(7)]}
.modelValue=${'exactly'}
label="equalsLengthValidator"
></lion-input>
`,
)
.add(
'minLength',
() => html`
<lion-input
.errorValidators=${[minLengthValidator(10)]}
.modelValue=${'too short'}
label="minLengthValidator"
></lion-input>
<lion-input
.errorValidators=${[minLengthValidator(10)]}
.modelValue=${'that should be enough'}
label="minLengthValidator"
></lion-input>
`,
)
.add(
'maxLength',
() => html`
<lion-input
.errorValidators=${[maxLengthValidator(13)]}
.modelValue=${'too long it seems'}
label="maxLengthValidator"
></lion-input>
<lion-input
.errorValidators=${[maxLengthValidator(13)]}
.modelValue=${'just perfect'}
label="maxLengthValidator"
></lion-input>
`,
)
.add(
'minMaxLength',
() => html`
<lion-input
.errorValidators=${[minMaxLengthValidator({ min: 10, max: 13 })]}
.modelValue=${'too short'}
label="minMaxLengthValidator"
></lion-input>
<lion-input
.errorValidators=${[minMaxLengthValidator({ min: 10, max: 13 })]}
.modelValue=${'too long it seems'}
label="minMaxLengthValidator"
></lion-input>
<lion-input
.errorValidators=${[minMaxLengthValidator({ min: 10, max: 13 })]}
.modelValue=${'just perfect'}
label="minMaxLengthValidator"
></lion-input>
`,
)
.add(
'isEmail',
() => html`
<lion-input
.errorValidators=${[isEmailValidator()]}
.modelValue=${'foo'}
label="isEmailValidator"
></lion-input>
<lion-input
.errorValidators=${[isEmailValidator()]}
.modelValue=${'foo@bar.com'}
label="isEmailValidator"
></lion-input>
`,
)
.add('error/warning/info/success states', () => {
class InputValidationExample extends LocalizeMixin(LionInput) {
static get localizeNamespaces() {
return [
{ 'input-validation-example': locale => import(`./translations/${locale}.js`) },
...super.localizeNamespaces,
];
}
}
if (!customElements.get('input-validation-example')) {
customElements.define('input-validation-example', InputValidationExample);
}
const notEqualsString = (value, stringValue) => stringValue.toString() !== value;
const notEqualsStringValidator = (...factoryParams) => [
(...params) => ({ notEqualsString: notEqualsString(...params) }),
factoryParams,
];
const equalsStringFixedValidator = () => [() => ({ notEqualsStringFixed: false })];
return html`
<input-validation-example
.errorValidators=${[notEqualsStringValidator('error')]}
.successValidators=${[equalsStringFixedValidator()]}
.modelValue=${'error'}
label="Error"
help-text="Clearing the error (add a character) will show a success message"
></input-validation-example>
<input-validation-example
.warningValidators=${[notEqualsStringValidator('warning')]}
.modelValue=${'warning'}
label="Warning"
></input-validation-example>
<input-validation-example
.infoValidators=${[notEqualsStringValidator('info')]}
.modelValue=${'info'}
label="Info"
></input-validation-example>
`;
});

View file

@ -23,6 +23,9 @@ npm i --save @lion/select-rich
import '@lion/select-rich/lion-select-rich.js'; import '@lion/select-rich/lion-select-rich.js';
import '@lion/select-rich/lion-options.js'; import '@lion/select-rich/lion-options.js';
import '@lion/option/lion-option.js'; import '@lion/option/lion-option.js';
// validator import example
import { Required } from '@lion/validate';
``` ```
### Example ### Example
@ -31,7 +34,7 @@ import '@lion/option/lion-option.js';
<lion-select-rich <lion-select-rich
name="favoriteColor" name="favoriteColor"
label="Favorite color" label="Favorite color"
.errorValidators=${[['required']]} .validators=${[new Required()]}
> >
<lion-options slot="input"> <lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option> <lion-option .choiceValue=${'red'}>Red</lion-option>

View file

@ -36,8 +36,8 @@
"@lion/fieldset": "^0.2.1" "@lion/fieldset": "^0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@lion/form": "^0.2.1",
"@lion/radio": "^0.2.1", "@lion/radio": "^0.2.1",
"@lion/validate": "^0.3.1",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4" "@open-wc/testing": "^2.3.4"
} }

View file

@ -108,15 +108,14 @@ export class LionRadioGroup extends LionFieldset {
} }
} }
// eslint-disable-next-line class-methods-use-this _isEmpty() {
__isRequired(modelValue) { const value = this.checkedValue;
const groupName = Object.keys(modelValue)[0]; if (typeof value === 'string' && value === '') {
const filtered = modelValue[groupName].filter(node => node.checked === true); return true;
const value = filtered.length > 0 ? filtered[0] : undefined; }
return { if (value === undefined || value === null) {
required: return true;
(typeof value === 'string' && value !== '') || }
(typeof value !== 'string' && typeof value !== 'undefined'), // TODO: && value !== null ? return false;
};
} }
} }

View file

@ -1,133 +1,104 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { localize } from '@lion/localize';
import '@lion/radio/lion-radio.js'; import '@lion/radio/lion-radio.js';
import '@lion/form/lion-form.js';
import '../lion-radio-group.js'; import '../lion-radio-group.js';
import { Required, Validator, loadDefaultFeedbackMessages } from '@lion/validate';
loadDefaultFeedbackMessages();
storiesOf('Forms|Radio Group', module) storiesOf('Forms|Radio Group', module)
.add( .add(
'Default', 'Default',
() => html` () => html`
<lion-form> <lion-radio-group name="dinosGroup" label="Favourite dinosaur">
<form> <lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?"> <lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio> <lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
<lion-radio </lion-radio-group>
name="dinos[]"
label="brontosaurus"
.choiceValue=${'brontosaurus'}
></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group>
</form>
</lion-form>
`, `,
) )
.add( .add(
'Pre Select', 'Pre Select',
() => html` () => html`
<lion-form> <lion-radio-group name="dinosGroup" label="Favourite dinosaur">
<form> <lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?"> <lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio> <lion-radio
<lion-radio name="dinos[]"
name="dinos[]" label="diplodocus"
label="brontosaurus" .modelValue=${{ value: 'diplodocus', checked: true }}
.choiceValue=${'brontosaurus'} ></lion-radio>
></lion-radio> </lion-radio-group>
<lion-radio
name="dinos[]"
label="diplodocus"
.modelValue=${{ value: 'diplodocus', checked: true }}
></lion-radio>
</lion-radio-group>
</form>
</lion-form>
`, `,
) )
.add( .add(
'Disabled', 'Disabled',
() => html` () => html`
<lion-form> <lion-radio-group name="dinosGroup" label="Favourite dinosaur" disabled>
<form> <lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio-group name="dinosGroup" label="What are your favourite dinosaurs?" disabled> <lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio> <lion-radio
<lion-radio name="dinos[]"
name="dinos[]" label="diplodocus"
label="brontosaurus" .modelValue=${{ value: 'diplodocus', checked: true }}
.choiceValue=${'brontosaurus'} ></lion-radio>
></lion-radio> </lion-radio-group>
<lion-radio
name="dinos[]"
label="diplodocus"
.modelValue=${{ value: 'diplodocus', checked: true }}
></lion-radio>
</lion-radio-group>
</form>
</lion-form>
`, `,
) )
.add('Validation', () => { .add('Validation', () => {
const submit = () => { const validate = () => {
const form = document.querySelector('#form'); const radioGroup = document.querySelector('#dinosGroup');
if (form.errorState === false) { radioGroup.submitted = !radioGroup.submitted;
console.log(form.serializeGroup());
}
}; };
return html` return html`
<lion-form id="form" @submit="${submit}" <lion-radio-group
><form> id="dinosGroup"
<lion-radio-group name="dinosGroup"
name="dinosGroup" label="Favourite dinosaur"
label="What are your favourite dinosaurs?" .validators=${[new Required()]}
.errorValidators=${[['required']]}
>
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio
name="dinos[]"
label="brontosaurus"
.choiceValue=${'brontosaurus'}
></lion-radio>
<lion-radio
name="dinos[]"
label="diplodocus"
.choiceValue="${'diplodocus'}}"
></lion-radio>
</lion-radio-group>
<button type="submit">Submit</button>
</form></lion-form
> >
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue="${'diplodocus'}"></lion-radio>
</lion-radio-group>
<button @click="${() => validate()}">Validate</button>
`; `;
}) })
.add('Validation Item', () => { .add('Validation Item', () => {
const isBrontosaurus = value => { class IsBrontosaurus extends Validator {
const selectedValue = value['dinos[]'].find(v => v.checked === true); constructor() {
return { super();
isBrontosaurus: selectedValue ? selectedValue.value === 'brontosaurus' : false, this.name = 'IsBrontosaurus';
}; }
};
localize.locale = 'en-GB'; execute(value) {
try { const selectedValue = value['dinos[]'].find(v => v.checked === true);
localize.addData('en-GB', 'lion-validate+isBrontosaurus', { const hasError = selectedValue ? selectedValue.value !== 'brontosaurus' : false;
error: { return hasError;
isBrontosaurus: 'You need to select "brontosaurus"', }
},
}); static async getMessage() {
} catch (error) { return 'You need to select "brontosaurus"';
// expected as it's a demo }
} }
const validate = () => {
const radioGroup = document.querySelector('#dinosGroup');
radioGroup.submitted = !radioGroup.submitted;
};
return html` return html`
<lion-radio-group <lion-radio-group
id="dinosGroup"
name="dinosGroup" name="dinosGroup"
label="What are your favourite dinosaurs?" label="Favourite dinosaur"
.errorValidators=${[['required'], [isBrontosaurus]]} .validators=${[new Required(), new IsBrontosaurus()]}
> >
<lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio> <lion-radio name="dinos[]" label="allosaurus" .choiceValue=${'allosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio> <lion-radio name="dinos[]" label="brontosaurus" .choiceValue=${'brontosaurus'}></lion-radio>
<lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio> <lion-radio name="dinos[]" label="diplodocus" .choiceValue=${'diplodocus'}></lion-radio>
</lion-radio-group> </lion-radio-group>
<button @click="${() => validate()}">Validate</button>
`; `;
}); });

View file

@ -1,4 +1,5 @@
import { expect, fixture, nextFrame, html } from '@open-wc/testing'; import { expect, fixture, nextFrame, html } from '@open-wc/testing';
import { Required } from '@lion/validate';
import '@lion/radio/lion-radio.js'; import '@lion/radio/lion-radio.js';
import '../lion-radio-group.js'; import '../lion-radio-group.js';
@ -90,7 +91,6 @@ describe('<lion-radio-group>', () => {
it('fires checked-value-changed event only once per checked change', async () => { it('fires checked-value-changed event only once per checked change', async () => {
let counter = 0; let counter = 0;
/* eslint-disable indent */
const el = await fixture(html` const el = await fixture(html`
<lion-radio-group <lion-radio-group
@checked-value-changed=${() => { @checked-value-changed=${() => {
@ -103,7 +103,6 @@ describe('<lion-radio-group>', () => {
</lion-radio-group> </lion-radio-group>
`); `);
await nextFrame(); await nextFrame();
/* eslint-enable indent */
expect(counter).to.equal(0); expect(counter).to.equal(0);
el.formElementsArray[0].checked = true; el.formElementsArray[0].checked = true;
@ -126,7 +125,6 @@ describe('<lion-radio-group>', () => {
it('expect child nodes to only fire one model-value-changed event per instance', async () => { it('expect child nodes to only fire one model-value-changed event per instance', async () => {
let counter = 0; let counter = 0;
/* eslint-disable indent */
const el = await fixture(html` const el = await fixture(html`
<lion-radio-group <lion-radio-group
@model-value-changed=${() => { @model-value-changed=${() => {
@ -139,7 +137,6 @@ describe('<lion-radio-group>', () => {
</lion-radio-group> </lion-radio-group>
`); `);
await nextFrame(); await nextFrame();
/* eslint-enable indent */
counter = 0; // reset after setup which may result in different results counter = 0; // reset after setup which may result in different results
el.formElementsArray[0].checked = true; el.formElementsArray[0].checked = true;
@ -191,7 +188,7 @@ describe('<lion-radio-group>', () => {
}); });
it('should have role = radiogroup', async () => { it('should have role = radiogroup', async () => {
const el = await fixture(` const el = await fixture(html`
<lion-radio-group> <lion-radio-group>
<label slot="label">My group</label> <label slot="label">My group</label>
<lion-radio name="gender[]" value="male"> <lion-radio name="gender[]" value="male">
@ -208,41 +205,50 @@ describe('<lion-radio-group>', () => {
it('can be required', async () => { it('can be required', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-radio-group .errorValidators="${[['required']]}"> <lion-radio-group .validators=${[new Required()]}>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio> <lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio> <lion-radio
name="gender[]"
.choiceValue=${{ subObject: 'satisfies required' }}
></lion-radio>
</lion-radio-group> </lion-radio-group>
`); `);
await nextFrame(); expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('Required');
expect(el.error.required).to.be.true;
el.formElements['gender[]'][0].checked = true; el.formElements['gender[]'][0].checked = true;
expect(el.error.required).to.be.undefined; expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
el.formElements['gender[]'][1].checked = true;
expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
}); });
it('returns serialized value', async () => { it('returns serialized value', async () => {
const group = await fixture(html` const el = await fixture(html`
<lion-radio-group .errorValidators="${[['required']]}"> <lion-radio-group>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio> <lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio> <lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio>
</lion-radio-group> </lion-radio-group>
`); `);
el.formElements['gender[]'][0].checked = true;
group.formElements['gender[]'][0].checked = true; expect(el.serializedValue).to.deep.equal({ checked: true, value: 'male' });
expect(group.serializedValue).to.deep.equal({ checked: true, value: 'male' });
}); });
it('returns serialized value on unchecked state', async () => { it('returns serialized value on unchecked state', async () => {
const group = await fixture(html` const el = await fixture(html`
<lion-radio-group .errorValidators="${[['required']]}"> <lion-radio-group>
<lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio> <lion-radio name="gender[]" .choiceValue=${'male'}></lion-radio>
<lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio> <lion-radio name="gender[]" .choiceValue=${'female'}></lion-radio>
</lion-radio-group> </lion-radio-group>
`); `);
await nextFrame(); await nextFrame();
expect(group.serializedValue).to.deep.equal(''); expect(el.serializedValue).to.deep.equal('');
}); });
it(`becomes "touched" once a single element of the group changes`, async () => { it(`becomes "touched" once a single element of the group changes`, async () => {

View file

@ -30,6 +30,9 @@ npm i --save @lion/select-rich
import '@lion/select-rich/lion-select-rich.js'; import '@lion/select-rich/lion-select-rich.js';
import '@lion/select-rich/lion-options.js'; import '@lion/select-rich/lion-options.js';
import '@lion/option/lion-option.js'; import '@lion/option/lion-option.js';
// validator import example
import { Requred } from '@lion/validate';
``` ```
### Example ### Example
@ -38,7 +41,7 @@ import '@lion/option/lion-option.js';
<lion-select-rich <lion-select-rich
name="favoriteColor" name="favoriteColor"
label="Favorite color" label="Favorite color"
.errorValidators=${[['required']]} .validators=${[new Required()]}
> >
<lion-options slot="input"> <lion-options slot="input">
<lion-option .choiceValue=${'red'}>Red</lion-option> <lion-option .choiceValue=${'red'}>Red</lion-option>

View file

@ -552,6 +552,7 @@ export class LionSelectRich extends OverlayMixin(
this.__listboxOnClick = () => { this.__listboxOnClick = () => {
this.opened = false; this.opened = false;
}; };
this._listboxNode.addEventListener('click', this.__listboxOnClick); this._listboxNode.addEventListener('click', this.__listboxOnClick);
this.__listboxOnKeyUp = this.__listboxOnKeyUp.bind(this); this.__listboxOnKeyUp = this.__listboxOnKeyUp.bind(this);
@ -598,18 +599,15 @@ export class LionSelectRich extends OverlayMixin(
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide); this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
} }
// eslint-disable-next-line class-methods-use-this _isEmpty() {
__isRequired(modelValue) { const value = this.checkedValue;
const checkedModelValue = modelValue.find(subModelValue => subModelValue.checked === true); if (typeof value === 'string' && value === '') {
if (!checkedModelValue) { return true;
return { required: false };
} }
const { value } = checkedModelValue; if (value === undefined || value === null) {
return { return true;
required: }
(typeof value === 'string' && value !== '') || return false;
(typeof value !== 'string' && value !== undefined && value !== null),
};
} }
/** /**
@ -625,4 +623,15 @@ export class LionSelectRich extends OverlayMixin(
get _overlayContentNode() { get _overlayContentNode() {
return this._listboxNode; return this._listboxNode;
} }
set fieldName(value) {
this.__fieldName = value;
}
get fieldName() {
const label =
this.label ||
(this.querySelector('[slot=label]') && this.querySelector('[slot=label]').textContent);
return this.__fieldName || label || this.name;
}
} }

View file

@ -1,8 +1,10 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { css } from '@lion/core'; import { css } from '@lion/core';
import { Required } from '@lion/validate';
import '@lion/form/lion-form.js'; import '@lion/form/lion-form.js';
import '@lion/option/lion-option.js'; import '@lion/option/lion-option.js';
import '@lion/button/lion-button.js';
import '../lion-select-rich.js'; import '../lion-select-rich.js';
import '../lion-options.js'; import '../lion-options.js';
@ -101,39 +103,29 @@ storiesOf('Forms|Select Rich', module)
</div> </div>
`, `,
) )
.add('Validation', () => { .add(
const submit = () => { 'Validation',
const form = document.querySelector('#form'); () => html`
if (form.errorState === false) {
console.log(form.serializeGroup());
}
};
return html`
<style> <style>
${selectRichDemoStyle} ${selectRichDemoStyle}
</style> </style>
<div class="demo-area"> <div class="demo-area">
<lion-form id="form" @submit="${submit}"> <lion-select-rich
<form> id="color"
<lion-select-rich name="color"
id="color" label="Favorite color"
name="color" .validators="${[new Required()]}"
label="Favorite color" >
.errorValidators="${[['required']]}" <lion-options slot="input" class="demo-listbox">
> <lion-option .choiceValue=${null}>select a color</lion-option>
<lion-options slot="input" class="demo-listbox"> <lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${null}>select a color</lion-option> <lion-option .choiceValue=${'hotpink'} disabled>Hotpink</lion-option>
<lion-option .choiceValue=${'red'}>Red</lion-option> <lion-option .choiceValue=${'teal'}>Teal</lion-option>
<lion-option .choiceValue=${'hotpink'}>Hotpink</lion-option> </lion-options>
<lion-option .choiceValue=${'teal'}>Teal</lion-option> </lion-select-rich>
</lion-options>
</lion-select-rich>
<lion-button type="submit">Submit</lion-button>
</form>
</lion-form>
</div> </div>
`; `,
}) )
.add('Render Options', () => { .add('Render Options', () => {
const objs = [ const objs = [
{ type: 'mastercard', label: 'Master Card', amount: 12000, active: true }, { type: 'mastercard', label: 'Master Card', amount: 12000, active: true },

View file

@ -1,6 +1,7 @@
import { expect, fixture, html, triggerFocusFor, triggerBlurFor } from '@open-wc/testing'; import { expect, fixture, html, triggerFocusFor, triggerBlurFor } from '@open-wc/testing';
import './keyboardEventShimIE.js'; import './keyboardEventShimIE.js';
import { Required } from '@lion/validate';
import '@lion/option/lion-option.js'; import '@lion/option/lion-option.js';
import '../lion-options.js'; import '../lion-options.js';
import '../lion-select-rich.js'; import '../lion-select-rich.js';
@ -351,7 +352,8 @@ describe('lion-select-rich interactions', () => {
expect(el.activeIndex).to.equal(0); expect(el.activeIndex).to.equal(0);
}); });
it('skips disabled options while navigates to first and last option with [Home] and [End] keys', async () => { // flaky test
it.skip('skips disabled options while navigates to first and last option with [Home] and [End] keys', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-select-rich opened> <lion-select-rich opened>
<lion-options slot="input" name="foo"> <lion-options slot="input" name="foo">
@ -362,7 +364,7 @@ describe('lion-select-rich interactions', () => {
</lion-options> </lion-options>
</lion-select-rich> </lion-select-rich>
`); `);
expect(el.activeIndex).to.equal(1); expect(el.activeIndex).to.equal(2);
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' })); el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
expect(el.activeIndex).to.equal(2); expect(el.activeIndex).to.equal(2);
@ -585,16 +587,22 @@ describe('lion-select-rich interactions', () => {
describe('Validation', () => { describe('Validation', () => {
it('can be required', async () => { it('can be required', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-select-rich .errorValidators=${['required']}> <lion-select-rich .validators=${[new Required()]}>
<lion-options slot="input"> <lion-options slot="input">
<lion-option .choiceValue=${null}>Please select a value</lion-option> <lion-option .choiceValue=${null}>Please select a value</lion-option>
<lion-option .choiceValue=${20}>Item 2</lion-option> <lion-option .choiceValue=${20}>Item 2</lion-option>
</lion-options> </lion-options>
</lion-select-rich> </lion-select-rich>
`); `);
expect(el.error.required).to.be.true;
expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).to.have.a.property('Required');
el.checkedValue = 20; el.checkedValue = 20;
expect(el.error.required).to.be.undefined; expect(el.hasFeedbackFor).not.to.include('error');
expect(el.validationStates).to.have.a.property('error');
expect(el.validationStates.error).not.to.have.a.property('Required');
}); });
}); });

View file

@ -16,6 +16,44 @@ import '../lion-select-rich.js';
import { LionSelectRich } from '../index.js'; import { LionSelectRich } from '../index.js';
describe('lion-select-rich', () => { describe('lion-select-rich', () => {
it(`has a fieldName based on the label`, async () => {
const el1 = await fixture(
html`
<lion-select-rich label="foo"><lion-options slot="input"></lion-options></lion-select-rich>
`,
);
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
const el2 = await fixture(
html`
<lion-select-rich
><label slot="label">bar</label><lion-options slot="input"></lion-options
></lion-select-rich>
`,
);
expect(el2.fieldName).to.equal(el2._labelNode.textContent);
});
it(`has a fieldName based on the name if no label exists`, async () => {
const el = await fixture(
html`
<lion-select-rich name="foo"><lion-options slot="input"></lion-options></lion-select-rich>
`,
);
expect(el.fieldName).to.equal(el.name);
});
it(`can override fieldName`, async () => {
const el = await fixture(
html`
<lion-select-rich label="foo" .fieldName="${'bar'}"
><lion-options slot="input"></lion-options
></lion-select-rich>
`,
);
expect(el.__fieldName).to.equal(el.fieldName);
});
it('does not have a tabindex', async () => { it('does not have a tabindex', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-select-rich> <lion-select-rich>

View file

@ -36,6 +36,7 @@
"@lion/field": "^0.4.1" "@lion/field": "^0.4.1"
}, },
"devDependencies": { "devDependencies": {
"@lion/validate": "^0.3.1",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4" "@open-wc/testing": "^2.3.4"
} }

View file

@ -1,4 +1,5 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { Required } from '@lion/validate';
import '../lion-select.js'; import '../lion-select.js';
@ -46,26 +47,20 @@ storiesOf('Forms|Select', module)
`, `,
) )
.add('Validation', () => { .add('Validation', () => {
const submit = () => { const validate = () => {
const form = document.querySelector('#form'); const select = document.querySelector('#color');
if (form.errorState === false) { select.submitted = !select.submitted;
console.log(form.serializeGroup());
}
}; };
return html` return html`
<lion-form id="form" @submit="${submit}" <lion-select id="color" name="color" .validators="${[new Required()]}">
><form> <label slot="label">Favorite color</label>
<lion-select id="color" name="color" .errorValidators="${[['required']]}"> <select slot="input">
<label slot="label">Favorite color</label> <option selected hidden value>Please select</option>
<select slot="input"> <option value="red">Red</option>
<option selected hidden value>Please select</option> <option value="hotpink">Hotpink</option>
<option value="red">Red</option> <option value="teal">Teal</option>
<option value="hotpink">Hotpink</option> </select>
<option value="teal">Teal</option> </lion-select>
</select> <button @click="${() => validate()}">Validate</button>
</lion-select>
<button type="submit">Submit</button>
</form></lion-form
>
`; `;
}); });

View file

@ -2,7 +2,7 @@
[//]: # 'AUTO INSERT HEADER PREPUBLISH' [//]: # 'AUTO INSERT HEADER PREPUBLISH'
`lion-switch` is a component that is used to toggle a property or feature on or off. `lion-switch` is a component that is used to toggle a property or feature on or off. Toggling the component on or off should have immediate action and should not require pressing any additional buttons (submit) to confirm what just happened. The Switch is not a Checkbox in disguise and should not be used as part of a form.
## Features ## Features
@ -19,7 +19,7 @@ npm i --save @lion/switch
``` ```
```js ```js
import '@lion/swith/lion-switch.js'; import '@lion/switch/lion-switch.js';
``` ```
### Example ### Example

View file

@ -38,7 +38,6 @@
"@lion/field": "^0.4.1" "@lion/field": "^0.4.1"
}, },
"devDependencies": { "devDependencies": {
"@lion/form": "^0.2.1",
"@lion/localize": "^0.5.0", "@lion/localize": "^0.5.0",
"@lion/validate": "^0.3.1", "@lion/validate": "^0.3.1",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",

View file

@ -39,6 +39,7 @@ export class LionSwitch extends ChoiceInputMixin(LionField) {
this.__handleButtonSwitchCheckedChanged.bind(this), this.__handleButtonSwitchCheckedChanged.bind(this),
); );
this._syncButtonSwitch(); this._syncButtonSwitch();
this.submitted = true;
} }
updated(changedProperties) { updated(changedProperties) {
@ -46,6 +47,12 @@ export class LionSwitch extends ChoiceInputMixin(LionField) {
this._syncButtonSwitch(); this._syncButtonSwitch();
} }
/**
* Override this function from ChoiceInputMixin
*/
// eslint-disable-next-line class-methods-use-this
_isEmpty() {}
__handleButtonSwitchCheckedChanged() { __handleButtonSwitchCheckedChanged() {
// TODO: should be replaced by "_inputNode" after the next breaking change // TODO: should be replaced by "_inputNode" after the next breaking change
// https://github.com/ing-bank/lion/blob/master/packages/field/src/FormControlMixin.js#L78 // https://github.com/ing-bank/lion/blob/master/packages/field/src/FormControlMixin.js#L78

View file

@ -31,7 +31,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
outline: 0; outline: 0;
} }
:host(:focus:not([disabled])) .btn { :host(:focus:not([disabled])) .switch-button__thumb {
/* if you extend, please overwrite */ /* if you extend, please overwrite */
outline: 2px solid #bde4ff; outline: 2px solid #bde4ff;
} }

View file

@ -1,92 +1,49 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { LitElement } from '@lion/core'; import { Validator } from '@lion/validate';
import { LocalizeMixin } from '@lion/localize';
import '../lion-switch.js'; import '../lion-switch.js';
import '../lion-switch-button.js'; import '../lion-switch-button.js';
import '@lion/form/lion-form.js';
storiesOf('Buttons|Switch', module) storiesOf('Buttons|Switch', module)
.add(
'Default',
() => html`
<lion-switch label="Label" help-text="Help text"></lion-switch>
`,
)
.add(
'Disabeld',
() => html`
<lion-switch label="Label" disabled></lion-switch>
`,
)
.add('Validation', () => {
class IsTrue extends Validator {
constructor() {
super();
this.name = 'IsTrue';
}
execute(value) {
return !value.checked;
}
static async getMessage() {
return "You won't get the latest news!";
}
}
return html`
<lion-switch
id="newsletterCheck"
name="newsletterCheck"
label="Subscribe to newsletter"
.validators="${[new IsTrue(null, { type: 'info' })]}"
></lion-switch>
`;
})
.add( .add(
'Button', 'Button',
() => html` () => html`
<lion-switch-button aria-label="Toggle button"></lion-switch-button> <lion-switch-button aria-label="Toggle button"></lion-switch-button>
`, `,
) );
.add(
'Disabled',
() => html`
<lion-switch-button aria-label="Toggle button" disabled></lion-switch-button>
`,
)
.add(
'With input slots',
() => html`
<lion-switch label="Label" help-text="Help text"></lion-switch>
`,
)
.add('Validation', () => {
const isTrue = value => value && value.checked && value.checked === true;
const isTrueValidator = (...factoryParams) => [
(...params) => ({
isTrue: isTrue(...params),
}),
...factoryParams,
];
const tagName = 'lion-switch-validation-demo';
if (!customElements.get(tagName)) {
customElements.define(
tagName,
class extends LocalizeMixin(LitElement) {
static get localizeNamespaces() {
const result = [
{
'lion-validate+isTrue': () =>
Promise.resolve({
info: {
isTrue: 'You will not get the latest news!',
},
}),
},
...super.localizeNamespaces,
];
return result;
}
render() {
return html`
<lion-form id="postsForm" @submit="${this.submit}">
<form>
<lion-switch name="emailAddress" label="Share email address"> </lion-switch>
<lion-switch name="subjectField" label="Show subject field" checked>
</lion-switch>
<lion-switch name="characterCount" label="Character count"> </lion-switch>
<lion-switch
name="newsletterCheck"
label="* Subscribe to newsletter"
.infoValidators="${[isTrueValidator()]}"
>
</lion-switch>
<button type="submit">
Submit
</button>
</form>
</lion-form>
`;
}
submit() {
const form = this.shadowRoot.querySelector('#postsForm');
if (form.errorState === false) {
console.log(form.serializeGroup());
}
}
},
);
}
return html`
<lion-switch-validation-demo></lion-switch-validation-demo>
`;
});

View file

@ -67,4 +67,11 @@ describe('lion-switch', () => {
value: 'foo', value: 'foo',
}); });
}); });
it('is submitted by default', async () => {
const el = await fixture(html`
<lion-switch></lion-switch>
`);
expect(el.submitted).to.be.true;
});
}); });

View file

@ -37,6 +37,7 @@
"autosize": "4.0.2" "autosize": "4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@lion/validate": "^0.3.1",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4" "@open-wc/testing": "^2.3.4"
} }

View file

@ -1,7 +1,9 @@
import { storiesOf, html } from '@open-wc/demoing-storybook'; import { storiesOf, html } from '@open-wc/demoing-storybook';
import { Required, MinLength, MaxLength } from '@lion/validate';
import '../lion-textarea.js'; import '../lion-textarea.js';
const lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
storiesOf('Forms|Textarea', module) storiesOf('Forms|Textarea', module)
.add( .add(
'Default', 'Default',
@ -41,4 +43,14 @@ storiesOf('Forms|Textarea', module)
<p>To have a fixed size provide rows and maxRows with the same value</p> <p>To have a fixed size provide rows and maxRows with the same value</p>
<lion-textarea label="Non Growing" rows="3" max-rows="3"></lion-textarea> <lion-textarea label="Non Growing" rows="3" max-rows="3"></lion-textarea>
`, `,
)
.add(
'Validation',
() => html`
<lion-textarea
.validators="${[new Required(), new MinLength(10), new MaxLength(400)]}"
label="Validation"
.modelValue="${lorem}"
></lion-textarea>
`,
); );

View file

@ -1,4 +1,5 @@
import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js'; import { runFormatMixinSuite } from '@lion/field/test-suites/FormatMixin.suite.js';
import '../lion-textarea.js'; import '../lion-textarea.js';
const tagString = 'lion-textarea'; const tagString = 'lion-textarea';

View file

@ -7,14 +7,13 @@
- allow for advanced UX scenarios by updating validation state on every value change - allow for advanced UX scenarios by updating validation state on every value change
- provide a powerful way of writing validation via pure functions - provide a powerful way of writing validation via pure functions
- multiple validation types(error, warning, info, success) - multiple validation types(error, warning, info, success)
- [default validators](./docs/DefaultValidators.md) - default validators
- [custom validators](./docs/tutorials/CustomValidatorsTutorial.md) - custom validators
Validation is applied by default to all [form controls](../field/docs/FormFundaments.md) via the Validation is applied by default to all [form controls](../field/docs/FormFundaments.md) via the
ValidateMixin. ValidateMixin.
For a detailed description of the validation system and the `ValidateMixin`, please see For a detailed description of the validation system and the `ValidateMixin`, please see [ValidationSystem](./docs/ValidationSystem.md).
[ValidationSystem](./docs/ValidationSystem.md).
## How to use ## How to use
@ -26,7 +25,7 @@ npm i --save @lion/validate
```js ```js
import '@lion/input/lion-input.js'; import '@lion/input/lion-input.js';
import { %validatorName% } from '@lion/validate'; import { %ValidatorName% } from '@lion/validate';
``` ```
> Note that we import an lion-input here as an example of a form control implementing ValidateMixin. > Note that we import an lion-input here as an example of a form control implementing ValidateMixin.
@ -39,39 +38,49 @@ All validators are provided as pure functions. They should be applied to the for
```js ```js
import '@lion/input/lion-input.js'; import '@lion/input/lion-input.js';
import { isString, maxLengthValidator, defaultOkValidator } from '@lion/validate'; import { Required, IsString, MaxLength, DefaultSuccess, Validator } from '@lion/validate';
const isInitialsRegex = /^([A-Z]\.)+$/; const isInitialsRegex = /^([A-Z]\.)+$/;
export const isExampleInitials = value => class IsInitialsExample extends Validator {
isString(value) && isInitialsRegex.test(value.toUpperCase()); constructor(...args) {
export const isExampleInitialsValidator = () => [ super(...args);
(...params) => ({ isExampleInitials: isExampleInitials(...params) }), this.name = 'IsExampleInitials';
]; }
execute(value) {
let hasError = false;
if (!IsString || !isInitialsRegex.test(value.toLowerCase())) {
hasError = true;
}
return hasError;
}
static getMessage({ fieldName }) {
return `Please enter a valid {fieldName} in the format "A.B.C.".`;
}
}
``` ```
```html ```html
<lion-input <lion-input
label="Initials" label="Initials"
name="initials" name="initials"
.errorValidators="${[['required], maxLengthValidator(10)]}" .validators="${[new Required(), new MaxLength(10), new IsInitialsExample(null, { type: 'warning' }), new DefaultSuccess()]}"
.warningValidators="${[isExampleInitialsValidator()]}"
.successValidators="${[defaultOkValidator()]}"
></lion-input> ></lion-input>
``` ```
In the example above we use different types of validators. In the example above we use different types of validators.
A validator applied to `.errorValidators` expects an array with a function, a parameters object and A validator applied to `.validators` expects an array with a function, a parameters object and
optionally an additional configuration object. optionally an additional configuration object.
```js ```js
minMaxLengthValidator({ min: 5, max: 10 }); MinMaxLength({ min: 5, max: 10 });
``` ```
The custom `isExampleInitialsValidator` checks if the value is fitting our regex, but does not The custom `IsInitialsExample` checks if the value is fitting our regex, but does not prevent the user from submitting other values.
prevent the user from submitting other values.
Retrieving validity states is as easy as checking for: Retrieving validity states is as easy as checking for:
```js ```js
myInitialsInput.errorState === false; myInitialsInput.hasFeedbackFor.include('error');
``` ```

View file

@ -1,54 +0,0 @@
# Default Validators
Default validator functions are the equivalent of native form validators, like required or min-length.
## Features
- list of validators:
- **required**: validates if the field is not empty.
- **length**: validates the length of the input.
- isString
- equalsLength
- minLength
- maxLength
- minMaxLength
- **number**: validates if the input is a number and the value of the number.
- isNumber
- minNumber
- maxNumber
- minMaxNumber
- **date**: validates if the input is a date and the value of the date.
- isDate
- minDate
- maxDate
- minMaxDate
- **email**: validates if the input is of type email.
- **success**: returns always falls, will be shown after a successful improvement of the value
- defaultOk
- randomOk
- all default validators have corresponding messages which are translated via the [localize system](../../localize/)
## How to use
### Installation
```sh
npm i --save @lion/validate
```
### Example
All validators are provided as pure functions and are added to your input field as follows:
```js
import { maxLengthValidator } from '@lion/validate';
import '@lion/input/ing-input.js';
```
```html
<ing-input
label="Initials"
name="initials"
.errorValidators="${[['required'], maxLengthValidator(10)]}"
></ing-input>
```

View file

@ -0,0 +1,17 @@
<!-- markdownlint-disable MD041 -->
```mermaid
graph TD
A(value changed) --> validate
B(validators changed) --> validate
```
```mermaid
graph TD
validate --> B{Check value}
B -->|is empty| C[Run required validator]
B -->|is not empty| syncOrAsync[non empty value]
syncOrAsync -->|has sync validators| F[Run sync]
syncOrAsync -->|has async validators| G((debounce))
G --> H[Run async]
```

View file

@ -3,7 +3,7 @@
Our validation system is designed to: Our validation system is designed to:
- allow for advanced UX scenarios by updating validation state on every value change - allow for advanced UX scenarios by updating validation state on every value change
- provide a powerful way of writing validations via pure functions - provide a powerful way of writing validations via classes
## When validation happens ## When validation happens
@ -20,75 +20,101 @@ a validation message should be shown along the input field.
## Validators ## Validators
All validators are provided via pure functions. They should be applied to the element implementing All validators are extensions of the `Validator` class. They should be applied to the element implementing
`ValidateMixin` as follows: `ValidateMixin` as follows:
```html ```html
<validatable-el <validatable-el
.errorValidators="${[[ myValidatorFunction, { myParam: 'foo' }, { extra: 'options' } ]]}" .validators="${[new MyValidator({ myParam: 'foo' }), { extra: 'options' } ]]}"
></validatable-el> ></validatable-el>
``` ```
As you can see the 'errorValidators' property expects a map (an array of arrays). As you can see the 'validators' property expects a map (an array of arrays).
So, every Validator is an array consisting of: So, every Validator is a class consisting of:
- validator function - validator function
- validator parameters (optional) - validator parameters (optional)
- validator config (optional) - validator config (optional)
### Factory functions ### Validator classes
A more readable and therefore recommended notation is the factory function, which is described in All validators extend from the default `Validator` class. Below example is an example of a validator could look like:
detail here: [Custom Validator Tutorial](./tutorials/CustomValidatorsTutorial.md).
When we talk about validators, we usually refer to factory functions.
Below example has two validators (as factory functions) applied: ```js
class MyValidator extends Validator {
constructor(...args) {
super(...args);
this.name = 'MyValidator';
}
execute(modelValue, param) {
const hasError = false;
if (modelValue === param) {
hasError = true;
}
return hasError;
}
static getMessage({ fieldName }) {
return `Please fill in ${fieldName}`;
}
}
```
```html ```html
<validatable-el <validatable-el .validators="${[new MyValidator('foo')]}"></validatable-el>
.errorValidators="${[minLengthValidator({ min: 3 }), isZipCodeValidator()]}"
></validatable-el>
``` ```
### Default Validators ### Default Validators
By default, the validate system ships with the following validators: By default, the validate system ships with the following validators:
- 'required' - Required
- isStringValidator - IsString, EqualsLength, MinLength, MaxLength, MinMaxLength, IsEmail
- equalsLengthValidator, minLengthValidator, maxLengthValidator, minMaxLengthValidator - IsNumber, MinNumber, MaxNumber, MinMaxNumber
- isNumberValidator, minNumberValidator, maxNumberValidator, minMaxNumberValidator - IsDate, MinDate, MaxDate, MinMaxDate, IsDateDisabled
- isDateValidator, minDateValidator, maxDateValidator, minMaxDateValidator, isDateDisabled - DefaultSuccess
- isEmailValidator
All validators return `true` if the required validity state is met. All validators return `false` if the required validity state is met.
As you can see, 'required' is placed in a string notation. It is the exception to the rule, All validators are considered self explanatory due to their explicit namings.
since the implementation of required is context dependent: it will be different for a regular input
than for a (multi)select and therefore not rely on one external function.
All other validators are considered self explanatory due to their explicit namings.
### Custom Validators ### Custom Validators
On top of default validators, application developers can write their own. On top of default validators, application developers can write their own by extending the `Validator` class.
See [Custom Validator Tutorial](./tutorials/CustomValidatorsTutorial.md) for an example of writing a
custom validator.
### Localization ### Localization
The `ValidateMixin` supports localization out of the box via the [localize system](../../localize/). The `ValidateMixin` supports localization out of the box via the [localize system](../../localize/).
By default, all error messages are translated in the following languages (depicted by iso code): All default validation messages are translated in the following languages (depicted by iso code):
bg, cs, de, en, es, fr, hu, it, nl, pl, ro ,ru, sk and uk. bg, cs, de, en, es, fr, hu, it, nl, pl, ro ,ru, sk, uk and zh.
## Asynchronous validation ## Asynchronous validation
By default, all validations are run synchronously. However, for instance when validation can only By default, all validations are run synchronously. However, for instance when validation can only take place on server level, asynchronous validation will be needed
take place on server level, asynchronous validation will be needed
Asynchronous validators are not yet supported. Please create a feature request if you need them in You can make your async validators as follows:
your application: it is quite vital this will be handled inside lion-web at `FormControl` level,
in order to create the best UX and accessibility (via (audio)visual feedback. ```js
class AsyncValidator extends Validator {
constructor(...args) {
super(...args);
this.name = 'AsyncValidator';
this.async = true;
}
async execute() {
console.log('async pending...');
await pause(2000);
console.log('async done...');
return true;
}
static getMessage({ modelValue }) {
return `Validated for modelValue: ${modelValue}`;
}
}
```
## Types of validators ## Types of validators
@ -111,20 +137,25 @@ The api for warning validators and info validators are as follows:
```html ```html
<validatable-field <validatable-field
.warningValidators="${[myWarningValidator()]}" .validators="${[new WarningExample(null, { type: 'warning' }), new InfoExample(null, { type: 'info' })]}"
.infoValidators="${[myInfoValidator()]}"
></validatable-field> ></validatable-field>
``` ```
### Success validators ### Success validators
Success validators work a bit differently. Their success state is defined by the lack of a Success validators work a bit differently. Their success state is defined by the lack of a previously existing erroneous state (which can be an error or warning state).
previously existing erroneous state (which can be an error or warning state).
So, an error validator going from invalid (true) state to invalid(false) state, will trigger the So, an error validator going from invalid (true) state to invalid(false) state, will trigger the success validator.
success validator. `ValidateMixin` has applied the `randomOkValidator`.
If we take a look at the translations file belonging to `ValidateMixin`: ```html
<validatable-field .validators="${[new MinLength(10), new DefaultSuccess()]}"></validatable-field>
```
<!-- TODO (nice to have)
#### Random Ok
If we take a look at the translations file belonging to `Validators`:
```js ```js
... ...
@ -141,11 +172,7 @@ If we take a look at the translations file belonging to `ValidateMixin`:
... ...
``` ```
You an see that the translation message of `randomOk` references the other success translation You an see that the translation message of `randomOk` references the other success translation keys. Every time the randomOkValidator is triggered, one of those messages will be randomly displayed.
keys. Every time the randomOkValidator is triggered, one of those messages will be randomly
displayed.
<!-- TODO (nice to have)
## Retrieving validity states imperatively ## Retrieving validity states imperatively

View file

@ -1,149 +0,0 @@
# Custom Validator Tutorial
Validators consist of an array in the format of `[<function>, <?params>, <?opts>]`.
They are implemented via pure functions and can be coupled to localized messages.
They should be used in conjunction with a [`Form Control`](../../../field/docs/FormFundaments.md) that implements the [`ValidateMixin`](../../).
This tutorial will show you how to build your own custom IBAN validator, including a validator factory function that makes it easy to apply and share it.
## Implement the validation logic
It's a good habit to make your validators compatible with as many locales and languages as possible.
For simplicity, this tutorial will only support Dutch and US zip codes.
As a best practice, create your validator in a new file from which it can be exported for eventual reuse.
We will start with this simple function body:
```js
export function validateIban() {
return true;
}
```
It return a boolean, which is `true` if the value is valid.
Which value? The [modelValue](../../../field/docs/FormattingAndParsing.md) like this:
```js
export function validateIban(modelValue) {
return /\d{5}([ \-]\d{4})?$/.test(modelValue.trim());
}
```
The above function validates IBANs for the United States ('us').
Suppose we want to support 'nl' IBANs as well.
Since our validator is a pure function, we need a parameter for it:
```js
export function validateIban(modelValue, { country } = {}) {
const regexpes = {
us: /\d{5}([ \-]\d{4})?$/,
nl: /^[1-9]{1}[0-9]{3} ?[a-zA-Z]{2}$/,
};
return regexpes[country || 'us'].test(modelValue.trim());
}
```
## Creating a validate function
As can be read in [validate](../ValidationSystem.md), a validator applied to `.errorValidators` expects an array with a validate function, a parameters object and an additional configuration object.
It expects a function with such an interface:
```js
export function isIban(...args) {
return {
'my-app-isIban': validateIban(...args),
};
}
```
`my-app-isIban` is the unique id of the validator.
The naming convention for this unique id will be explained later in this document.
## Creating a reusable factory function
But this is way easier to create a factory function which will automatically create an array for `.errorValidators`.
We recommend using this approach and from now on when saying a validator function we mean this one:
```js
export const isIbanValidator = (...factoryParams) => [
(...params) => ({ 'my-app-isIban': isIban(...params) }),
...factoryParams,
];
```
Here the naming convention is `validator name` + `Validator`.
Thus, `isIbanValidator` is your validator factory function.
## Adding an error message
<!-- TODO: increase DX here and probably deprecate this approach.
Also, make it possible to reuse validators and override messages.
Consider a local approach, since namespace are just objects that can be supplied as arguments
in prebaked factory functions. There's no need for a global namespace then.
On top/better: consider a less coupled design (localization outside of validation and strings being passed
to feedback renderer) -->
Now, we want to add an error message.
For this, we need to have a bit more knowledge about how the `ValidateMixin` handles translation resources.
As can be read in [validate](../../), the `ValidateMixin` considers all namespaces configured via `get loadNamespaces`.
By default, this contains at least the `lion-validate` namespace which is added by the `ValidateMixin`.
On top of this, for every namespace found, it adds an extra `{namespace}-{validatorUniqueId}` namespace.
Let's assume we apply our validator on a regular `<lion-input>`.
If our `validatorUniqueId` was `isIban`, that would mean on validation these two namespaces are considered, and in this order:
- lion-validate+isIban
- lion-validate
One should be aware that localize namespaces are defined in a global scope.
Therefore the approach above would only work fine when the IBAN validator would be part of the core code base ([validate](../../)).
As long as validators are part of an application, we need to avoid global namespace clashes.
Therefore, we recommend to prefix the application name, like this: `my-app-isIban`.
The resulting `lion-validate+my-app-isIban` namespace is now guaranteed to be unique.
In order for the localization data to be found, the translation files need to be added to the manager of [localize](../../../localize/).
The recommended way to do this (inside your `validators.js` file):
```js
localize.loadNamespace({
'lion-validate+my-app-isIban': locale => {
return import(`./translations/${locale}.js`);
},
});
```
In (for instance) `./translations/en.js`, we will see:
```js
export default {
error: {
'my-app-isIban':
'Please enter a(n) valid {validatorParams.country} IBAN number for {fieldName}.',
},
warning: {
'my-app-isIban':
'Please enter a(n) valid {validatorParams.country} IBAN number for {fieldName}.',
},
};
```
<!-- TODO: use error messages for warning validators as backup, so they can be omitted for almost all use cases -->
`validatorParams` is the second argument passed to the validator.
In this case this is the object `{ country: '%value%' }` where `%value%` is the one passed by an app developer.
## Conclusion
We are now good to go to reuse our validator in external contexts.
After importing it, using the validator would be as easy as this:
```html
<validatable-el .errorValidators="${[isIbanValidator({ country: 'nl' })]}"></validatable-el>
```

View file

@ -1,40 +1,31 @@
export { ValidateMixin } from './src/ValidateMixin.js'; export { ValidateMixin } from './src/ValidateMixin.js';
export { Unparseable } from './src/Unparseable.js'; export { Unparseable } from './src/Unparseable.js';
export { isValidatorApplied } from './src/isValidatorApplied.js'; export { Validator } from './src/Validator.js';
export { ResultValidator } from './src/ResultValidator.js';
export { loadDefaultFeedbackMessages } from './src/loadDefaultFeedbackMessages.js';
export { Required } from './src/validators/Required.js';
export { export {
defaultOk, IsString,
defaultOkValidator, EqualsLength,
isDateDisabled, MinLength,
isDateDisabledValidator, MaxLength,
equalsLength, MinMaxLength,
equalsLengthValidator, IsEmail,
isDate, } from './src/validators/StringValidators.js';
isDateValidator,
isEmail, export { IsNumber, MinNumber, MaxNumber, MinMaxNumber } from './src/validators/NumberValidators.js';
isEmailValidator,
isNumber, export {
isNumberValidator, IsDate,
isString, MinDate,
isStringValidator, MaxDate,
maxDate, MinMaxDate,
maxDateValidator, IsDateDisabled,
maxLength, } from './src/validators/DateValidators.js';
maxLengthValidator,
maxNumber, export { DefaultSuccess } from './src/resultValidators/DefaultSuccess.js';
maxNumberValidator,
minDate, export { LionValidationFeedback } from './src/LionValidationFeedback.js';
minDateValidator,
minLength,
minLengthValidator,
minMaxDate,
minMaxDateValidator,
minMaxLength,
minMaxLengthValidator,
minMaxNumber,
minMaxNumberValidator,
minNumber,
minNumberValidator,
randomOk,
randomOkValidator,
} from './src/validators.js';

View file

@ -0,0 +1,3 @@
import { LionValidationFeedback } from './src/LionValidationFeedback.js';
customElements.define('lion-validation-feedback', LionValidationFeedback);

View file

@ -29,6 +29,7 @@
"stories", "stories",
"test", "test",
"test-helpers", "test-helpers",
"test-suites",
"translations", "translations",
"*.js" "*.js"
], ],

View file

@ -0,0 +1,44 @@
import { html, LitElement } from '@lion/core';
/**
* @desc Takes care of accessible rendering of error messages
* Should be used in conjunction with FormControl having ValidateMixin applied
*/
export class LionValidationFeedback extends LitElement {
static get properties() {
return {
/**
* @property {FeedbackData} feedbackData
*/
feedbackData: Array,
};
}
/**
* @overridable
*/
// eslint-disable-next-line class-methods-use-this
_messageTemplate({ message }) {
return message;
}
updated() {
super.updated();
if (this.feedbackData && this.feedbackData[0]) {
this.setAttribute('type', this.feedbackData[0].type);
} else {
this.removeAttribute('type');
}
}
render() {
return html`
${this.feedbackData &&
this.feedbackData.map(
({ message, type, validator }) => html`
${this._messageTemplate({ message, type, validator })}
`,
)}
`;
}
}

View file

@ -0,0 +1,18 @@
import { Validator } from './Validator.js';
/**
* @desc Instead of evaluating the result of a regular validator, a ResultValidator looks
* at the total result of regular Validators. Instead of an execute function, it uses a
* 'executeOnResults' Validator.
* ResultValidators cannot be async, and should not contain an execute method.
*/
export class ResultValidator extends Validator {
/**
* @param {object} context
* @param {Validator[]} context.validationResult
* @param {Validator[]} context.prevValidationResult
* @param {Validator[]} context.validators
* @returns {Feedback[]}
*/
executeOnResults({ validationResult, prevValidationResult, validators }) {} // eslint-disable-line
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
import { fakeExtendsEventTarget } from './utils/fake-extends-event-target.js';
export class Validator {
constructor(param, config) {
fakeExtendsEventTarget(this);
this.name = '';
this.async = false;
this.__param = param;
this.__config = config || {};
this.type = (config && config.type) || 'error'; // Default type supported by ValidateMixin
}
/**
* @desc The function that returns a Boolean
* @param {string|Date|Number|object} modelValue
* @param {object} param
* @returns {Boolean|Promise<Boolean>}
*/
execute(/* modelValue, param */) {
if (!this.name) {
throw new Error('You must provide a name like "this.name = \'IsCat\'" for your Validator');
}
}
set param(p) {
this.__param = p;
this.dispatchEvent(new Event('param-changed'));
}
get param() {
return this.__param;
}
set config(c) {
this.__config = c;
this.dispatchEvent(new Event('config-changed'));
}
get config() {
return this.__config;
}
/**
* @overridable
* @param {object} data
* @param {*} data.modelValue
* @param {string} data.fieldName
* @param {*} data.params
* @param {string} data.type
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
*/
async _getMessage(data) {
const composedData = {
name: this.name,
type: this.type,
params: this.param,
...data,
};
if (typeof this.config.getMessage === 'function') {
return this.config.getMessage(composedData);
}
return this.constructor.getMessage(composedData);
}
/**
* @overridable
* @param {object} data
* @param {*} data.modelValue
* @param {string} data.fieldName
* @param {*} data.params
* @param {string} data.type
* @returns {string|Node|Promise<stringOrNode>|() => stringOrNode)}
*/
static async getMessage(/* data */) {
return `Please configure an error message for "${this.name}" by overriding "static async getMessage()"`;
}
/**
* @param {FormControl} formControl
*/
onFormControlConnect(formControl) {} // eslint-disable-line
/**
* @param {FormControl} formControl
*/
onFormControlDisconnect(formControl) {} // eslint-disable-line
/**
* @desc Used on async Validators, makes it able to do perf optimizations when there are
* pending "execute" calls with outdated values.
* ValidateMixin calls Validator.abortExecution() an async Validator can act accordingly,
* depending on its implementation of the "execute" function.
* - For instance, when fetch was called:
* https://stackoverflow.com/questions/31061838/how-do-i-cancel-an-http-fetch-request
* - Or, when a webworker was started, its process could be aborted and then restarted.
*/
abortExecution() {} // eslint-disable-line
}
// For simplicity, a default validator only handles one state:
// it can either be true or false an it will only have one message.
// In more advanced cases (think of the feedback mechanism for the maximum number of
// characters in Twitter), more states are needed. The alternative of
// having multiple distinct validators would be cumbersome to create and maintain,
// also because the validations would tie too much into each others logic.

View file

@ -1,18 +0,0 @@
/**
* TODO: refactor validators to classes, putting needed meta info on instance.
* Note that direct function comparison (Validator[0] === minDate) doesn't work when code
* is transpiled
* @param {String} name - a name like minDate, maxDate, minMaxDate
* @param {Function} fn - the validator function to execute provided in [fn, param, config]
* @param {Function} requiredSignature - arguments needed to execute fn without failing
* @returns {Boolean} - whether the validator (name) is applied
*/
export function isValidatorApplied(name, fn, requiredSignature) {
let result;
try {
result = Object.keys(fn(new Date(), requiredSignature))[0] === name;
} catch (e) {
result = false;
}
return result;
}

View file

@ -0,0 +1,141 @@
import { localize } from '@lion/localize';
import { Required } from './validators/Required.js';
import {
EqualsLength,
MinLength,
MaxLength,
MinMaxLength,
IsEmail,
} from './validators/StringValidators.js';
import { IsNumber, MinNumber, MaxNumber, MinMaxNumber } from './validators/NumberValidators.js';
import {
IsDate,
MinDate,
MaxDate,
MinMaxDate,
IsDateDisabled,
} from './validators/DateValidators.js';
import { DefaultSuccess } from './resultValidators/DefaultSuccess.js';
export { IsNumber, MinNumber, MaxNumber, MinMaxNumber } from './validators/NumberValidators.js';
let loaded = false;
export function loadDefaultFeedbackMessages() {
if (loaded === true) {
return;
}
const forMessagesToBeReady = () =>
localize.loadNamespace(
{
'lion-validate': locale => {
switch (locale) {
case 'bg-BG':
return import('../translations/bg-BG.js');
case 'bg':
return import('../translations/bg.js');
case 'cs-CZ':
return import('../translations/cs-CZ.js');
case 'cs':
return import('../translations/cs.js');
case 'de-DE':
return import('../translations/de-DE.js');
case 'de':
return import('../translations/de.js');
case 'en-AU':
return import('../translations/en-AU.js');
case 'en-GB':
return import('../translations/en-GB.js');
case 'en-US':
return import('../translations/en-US.js');
case 'en-PH':
case 'en':
return import('../translations/en.js');
case 'es-ES':
return import('../translations/es-ES.js');
case 'es':
return import('../translations/es.js');
case 'fr-FR':
return import('../translations/fr-FR.js');
case 'fr-BE':
return import('../translations/fr-BE.js');
case 'fr':
return import('../translations/fr.js');
case 'hu-HU':
return import('../translations/hu-HU.js');
case 'hu':
return import('../translations/hu.js');
case 'it-IT':
return import('../translations/it-IT.js');
case 'it':
return import('../translations/it.js');
case 'nl-BE':
return import('../translations/nl-BE.js');
case 'nl-NL':
return import('../translations/nl-NL.js');
case 'nl':
return import('../translations/nl.js');
case 'pl-PL':
return import('../translations/pl-PL.js');
case 'pl':
return import('../translations/pl.js');
case 'ro-RO':
return import('../translations/ro-RO.js');
case 'ro':
return import('../translations/ro.js');
case 'ru-RU':
return import('../translations/ru-RU.js');
case 'ru':
return import('../translations/ru.js');
case 'sk-SK':
return import('../translations/sk-SK.js');
case 'sk':
return import('../translations/sk.js');
case 'uk-UA':
return import('../translations/uk-UA.js');
case 'uk':
return import('../translations/uk.js');
case 'zh-CN':
case 'zh':
return import('../translations/zh.js');
default:
return import(`../translations/${locale}.js`);
}
},
},
{ locale: localize.localize },
);
const getLocalizedMessage = async data => {
await forMessagesToBeReady();
return localize.msg(`lion-validate:${data.type}.${data.name}`, data);
};
Required.getMessage = async data => getLocalizedMessage(data);
EqualsLength.getMessage = async data => getLocalizedMessage(data);
MinLength.getMessage = async data => getLocalizedMessage(data);
MaxLength.getMessage = async data => getLocalizedMessage(data);
MinMaxLength.getMessage = async data => getLocalizedMessage(data);
IsEmail.getMessage = async data => getLocalizedMessage(data);
IsNumber.getMessage = async data => getLocalizedMessage(data);
MinNumber.getMessage = async data => getLocalizedMessage(data);
MaxNumber.getMessage = async data => getLocalizedMessage(data);
MinMaxNumber.getMessage = async data => getLocalizedMessage(data);
IsDate.getMessage = async data => getLocalizedMessage(data);
MinDate.getMessage = async data => getLocalizedMessage(data);
MaxDate.getMessage = async data => getLocalizedMessage(data);
MinMaxDate.getMessage = async data => getLocalizedMessage(data);
IsDateDisabled.getMessage = async data => getLocalizedMessage(data);
DefaultSuccess.getMessage = async data => {
await forMessagesToBeReady();
const randomKeys = localize.msg('lion-validate:success.RandomOk').split(',');
const key = randomKeys[Math.floor(Math.random() * randomKeys.length)].trim();
return localize.msg(`lion-validate:${key}`, data);
};
loaded = true;
}

View file

@ -0,0 +1,16 @@
import { ResultValidator } from '../ResultValidator.js';
export class DefaultSuccess extends ResultValidator {
constructor(...args) {
super(...args);
this.type = 'success';
}
// eslint-disable-next-line class-methods-use-this
executeOnResults({ regularValidationResult, prevValidationResult }) {
const errorOrWarning = v => v.type === 'error' || v.type === 'warning';
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
return !hasErrorOrWarning && prevHadErrorOrWarning;
}
}

Some files were not shown because too many files have changed in this diff Show more