feat: synchronous form registration system
This commit is contained in:
parent
96b82b70b2
commit
8698f73418
25 changed files with 1581 additions and 1487 deletions
11
package.json
11
package.json
|
|
@ -25,8 +25,9 @@
|
|||
"storybook:build": "build-storybook",
|
||||
"storybook:build:start": "es-dev-server --root-dir storybook-static --open",
|
||||
"test": "run-p test:browser test:node",
|
||||
"test:browser": "wtr packages/**/*/test/**/*.test.js --coverage",
|
||||
"test:browser:watch": "wtr packages/**/*/test/**/*.test.js --watch",
|
||||
"test:browser": "wtr \"packages/**/*/test/**/*.test.js\" --coverage",
|
||||
"test:browser:watch": "wtr \"packages/**/*/test/**/*.test.js\" --watch",
|
||||
"test:browserstack": "wtr --config ./web-test-runner-browserstack.config.js \"packages/form-core/test/**/*.test.js\"",
|
||||
"test:node": "lerna run test:node"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -40,8 +41,10 @@
|
|||
"@open-wc/testing-helpers": "^1.0.0",
|
||||
"@storybook/addon-a11y": "~5.0.0",
|
||||
"@types/chai-dom": "^0.0.8",
|
||||
"@web/test-runner": "^0.6.33",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.5",
|
||||
"@web/dev-server-legacy": "^0.0.1",
|
||||
"@web/test-runner": "^0.6.40",
|
||||
"@web/test-runner-browserstack": "^0.0.6",
|
||||
"@webcomponents/webcomponentsjs": "^2.4.4",
|
||||
"babel-eslint": "^8.2.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bundlesize": "^0.17.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { LionField, IsNumber, Validator } from '@lion/form-core';
|
||||
import '@lion/form-core/lion-field.js';
|
||||
import { formFixture as fixture } from '@lion/form-core/test-helpers.js';
|
||||
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
||||
import {
|
||||
defineCE,
|
||||
|
|
@ -10,6 +9,8 @@ import {
|
|||
nextFrame,
|
||||
triggerFocusFor,
|
||||
unsafeStatic,
|
||||
fixture,
|
||||
aTimeout,
|
||||
} from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import '../lion-fieldset.js';
|
||||
|
|
@ -56,9 +57,9 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
it(`can override fieldName`, async () => {
|
||||
const el = await fixture(
|
||||
html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}>`,
|
||||
);
|
||||
const el = await fixture(html`
|
||||
<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}>
|
||||
`);
|
||||
expect(el.__fieldName).to.equal(el.fieldName);
|
||||
});
|
||||
|
||||
|
|
@ -187,9 +188,7 @@ describe('<lion-fieldset>', () => {
|
|||
<${tag} name="newfieldset">${inputSlots}</${tag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await el.registrationReady;
|
||||
const newFieldset = el.querySelector('lion-fieldset');
|
||||
await newFieldset.registrationReady;
|
||||
el.formElements.lastName.modelValue = 'Bar';
|
||||
newFieldset.formElements['hobbies[]'][0].modelValue = { checked: true, value: 'chess' };
|
||||
newFieldset.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'football' };
|
||||
|
|
@ -211,6 +210,9 @@ describe('<lion-fieldset>', () => {
|
|||
color: { checked: false, value: 'blue' },
|
||||
},
|
||||
});
|
||||
|
||||
// make sure values are full settled before changing them
|
||||
await aTimeout();
|
||||
el.modelValue = {
|
||||
lastName: 2,
|
||||
newfieldset: {
|
||||
|
|
@ -225,6 +227,7 @@ describe('<lion-fieldset>', () => {
|
|||
color: { checked: false, value: 'blue' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(newFieldset.formElements['hobbies[]'][0].modelValue).to.deep.equal({
|
||||
checked: true,
|
||||
value: 'chess',
|
||||
|
|
@ -250,10 +253,6 @@ describe('<lion-fieldset>', () => {
|
|||
</${tag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await el.registrationReady;
|
||||
const newFieldset = el.querySelector('lion-fieldset');
|
||||
await newFieldset.registrationReady;
|
||||
|
||||
expect(el.modelValue).to.deep.equal({
|
||||
b: 'x',
|
||||
newFieldset: {
|
||||
|
|
@ -298,12 +297,12 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
it('does not propagate/override initial disabled value on nested form elements', async () => {
|
||||
const el = await fixture(
|
||||
html`<${tag}>
|
||||
const el = await fixture(html`
|
||||
<${tag}>
|
||||
<${tag} name="sub" disabled>${inputSlots}</${tag}>
|
||||
</${tag}>`,
|
||||
);
|
||||
await el.updateComplete;
|
||||
</${tag}>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
expect(el.disabled).to.equal(false);
|
||||
expect(el.formElements.sub.disabled).to.be.true;
|
||||
expect(el.formElements.sub.formElements.color.disabled).to.be.true;
|
||||
|
|
@ -312,33 +311,25 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
it('can set initial modelValue on creation', async () => {
|
||||
const initialModelValue = {
|
||||
lastName: 'Bar',
|
||||
};
|
||||
const el = await fixture(html`
|
||||
<${tag} .modelValue=${initialModelValue}>
|
||||
<${tag} .modelValue=${{ lastName: 'Bar' }}>
|
||||
<${childTag} name="lastName"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
|
||||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
expect(el.modelValue).to.eql(initialModelValue);
|
||||
await el.registrationComplete;
|
||||
expect(el.modelValue).to.eql({
|
||||
lastName: 'Bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('can set initial serializedValue on creation', async () => {
|
||||
const initialSerializedValue = {
|
||||
lastName: 'Bar',
|
||||
};
|
||||
const el = await fixture(html`
|
||||
<${tag} .modelValue=${initialSerializedValue}>
|
||||
<${tag} .modelValue=${{ lastName: 'Bar' }}>
|
||||
<${childTag} name="lastName"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
|
||||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
expect(el.modelValue).to.eql(initialSerializedValue);
|
||||
await el.registrationComplete;
|
||||
expect(el.modelValue).to.eql({ lastName: 'Bar' });
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
|
|
@ -366,10 +357,10 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
it('validates when a value changes', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await nextFrame();
|
||||
const spy = sinon.spy(fieldset, 'validate');
|
||||
fieldset.formElements.color.modelValue = { checked: true, value: 'red' };
|
||||
const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await el.registrationComplete;
|
||||
const spy = sinon.spy(el, 'validate');
|
||||
el.formElements.color.modelValue = { checked: true, value: 'red' };
|
||||
expect(spy.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
|
|
@ -455,15 +446,16 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
it('sets touched when last field in fieldset left after focus', async () => {
|
||||
const fieldset = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await triggerFocusFor(fieldset.formElements['hobbies[]'][0]._inputNode);
|
||||
const el = await fixture(html`<${tag}>${inputSlots}</${tag}>`);
|
||||
await el.registrationComplete;
|
||||
await triggerFocusFor(el.formElements['hobbies[]'][0]._inputNode);
|
||||
await triggerFocusFor(
|
||||
fieldset.formElements['hobbies[]'][fieldset.formElements['gender[]'].length - 1]._inputNode,
|
||||
el.formElements['hobbies[]'][el.formElements['gender[]'].length - 1]._inputNode,
|
||||
);
|
||||
const el = await fixture(html`<button></button>`);
|
||||
el.focus();
|
||||
const button = await fixture(html`<button></button>`);
|
||||
button.focus();
|
||||
|
||||
expect(fieldset.touched).to.be.true;
|
||||
expect(el.touched).to.be.true;
|
||||
});
|
||||
|
||||
it('sets attributes [touched][dirty]', async () => {
|
||||
|
|
@ -611,22 +603,23 @@ describe('<lion-fieldset>', () => {
|
|||
});
|
||||
|
||||
it('(re)initializes children interaction states on registration ready', async () => {
|
||||
const fieldset = await fixtureSync(html`
|
||||
const el = await fixtureSync(html`
|
||||
<${tag} .modelValue="${{ a: '1', b: '2' }}">
|
||||
<${childTag} name="a"></${childTag}>
|
||||
<${childTag} name="b"></${childTag}>
|
||||
</${tag}>`);
|
||||
const childA = fieldset.querySelector('[name="a"]');
|
||||
const childB = fieldset.querySelector('[name="b"]');
|
||||
</${tag}>
|
||||
`);
|
||||
const childA = el.querySelector('[name="a"]');
|
||||
const childB = el.querySelector('[name="b"]');
|
||||
const spyA = sinon.spy(childA, 'initInteractionState');
|
||||
const spyB = sinon.spy(childB, 'initInteractionState');
|
||||
expect(fieldset.prefilled).to.be.false;
|
||||
expect(fieldset.dirty).to.be.false;
|
||||
await fieldset.registrationComplete;
|
||||
expect(el.prefilled).to.be.false;
|
||||
expect(el.dirty).to.be.false;
|
||||
await el.registrationComplete;
|
||||
expect(spyA).to.have.been.called;
|
||||
expect(spyB).to.have.been.called;
|
||||
expect(fieldset.prefilled).to.be.true;
|
||||
expect(fieldset.dirty).to.be.false;
|
||||
expect(el.prefilled).to.be.true;
|
||||
expect(el.dirty).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -917,7 +910,10 @@ describe('<lion-fieldset>', () => {
|
|||
}
|
||||
|
||||
execute(value) {
|
||||
const hasError = value.color.indexOf('a') === -1;
|
||||
let hasError = true;
|
||||
if (value && value.color) {
|
||||
hasError = value.color.indexOf('a') === -1;
|
||||
}
|
||||
return hasError;
|
||||
}
|
||||
}
|
||||
|
|
@ -928,7 +924,6 @@ describe('<lion-fieldset>', () => {
|
|||
<${childTag} name="color2"></${childTag}>
|
||||
</${tag}>
|
||||
`);
|
||||
await el.registrationReady;
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
expect(el.validationStates.error.ColorContainsA).to.be.true;
|
||||
expect(el.formElements.color.hasFeedbackFor).to.deep.equal([]);
|
||||
|
|
|
|||
|
|
@ -174,6 +174,11 @@ export const FormControlMixin = dedupeMixin(
|
|||
super.connectedCallback();
|
||||
this._enhanceLightDomClasses();
|
||||
this._enhanceLightDomA11y();
|
||||
this._triggerInitialModelValueChangedEvent();
|
||||
}
|
||||
|
||||
_triggerInitialModelValueChangedEvent() {
|
||||
this.__dispatchInitialModelValueChangedEvent();
|
||||
}
|
||||
|
||||
_enhanceLightDomClasses() {
|
||||
|
|
@ -590,19 +595,13 @@ export const FormControlMixin = dedupeMixin(
|
|||
return [...this.children].find(el => el.slot === slotName);
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__dispatchInitialModelValueChangedEvent();
|
||||
}
|
||||
|
||||
async __dispatchInitialModelValueChangedEvent() {
|
||||
__dispatchInitialModelValueChangedEvent() {
|
||||
// When we are not a fieldset / choice-group, we don't need to wait for our children
|
||||
// to send a unified event
|
||||
if (this._repropagationRole === 'child') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.registrationComplete;
|
||||
// Initially we don't repropagate model-value-changed events coming
|
||||
// from children. On firstUpdated we re-dispatch this event to maintain
|
||||
// 'count consistency' (to not confuse the application developer with a
|
||||
|
|
|
|||
|
|
@ -30,7 +30,14 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
set modelValue(value) {
|
||||
if (this.__isInitialModelValue) {
|
||||
this.__isInitialModelValue = false;
|
||||
this.registrationComplete.then(() => {
|
||||
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
|
||||
});
|
||||
} else {
|
||||
this._setCheckedElements(value, (el, val) => el.modelValue.value === val);
|
||||
}
|
||||
}
|
||||
|
||||
get serializedValue() {
|
||||
|
|
@ -50,13 +57,63 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
set serializedValue(value) {
|
||||
if (this.__isInitialSerializedValue) {
|
||||
this.__isInitialSerializedValue = false;
|
||||
this.registrationComplete.then(() => {
|
||||
this._setCheckedElements(value, (el, val) => el.serializedValue.value === val);
|
||||
});
|
||||
} else {
|
||||
this._setCheckedElements(value, (el, val) => el.serializedValue.value === val);
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.multipleChoice = false;
|
||||
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
||||
|
||||
this.__isInitialModelValue = true;
|
||||
this.__isInitialSerializedValue = true;
|
||||
this.registrationComplete = new Promise((resolve, reject) => {
|
||||
this.__resolveRegistrationComplete = resolve;
|
||||
this.__rejectRegistrationComplete = reject;
|
||||
});
|
||||
this.registrationComplete.done = false;
|
||||
this.registrationComplete.then(
|
||||
() => {
|
||||
this.registrationComplete.done = true;
|
||||
},
|
||||
() => {
|
||||
this.registrationComplete.done = true;
|
||||
throw new Error(
|
||||
'Registration could not finish. Please use await el.registrationComplete;',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.__registrationCompleteTimer = setTimeout(() => {
|
||||
this.__resolveRegistrationComplete();
|
||||
});
|
||||
|
||||
this.registrationComplete.then(() => {
|
||||
this.__isInitialModelValue = false;
|
||||
this.__isInitialSerializedValue = false;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
clearTimeout(this.__registrationCompleteTimer);
|
||||
if (this.registrationComplete.done === false) {
|
||||
this.__rejectRegistrationComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -68,6 +125,15 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
super.addFormElement(child, indexToInsertAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override from FormControlMixin
|
||||
*/
|
||||
_triggerInitialModelValueChangedEvent() {
|
||||
this.registrationComplete.then(() => {
|
||||
this.__dispatchInitialModelValueChangedEvent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
|
|
@ -129,11 +195,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
return this.formElements.filter(el => el.checked && !el.disabled);
|
||||
}
|
||||
|
||||
async _setCheckedElements(value, check) {
|
||||
if (!this.__readyForRegistration) {
|
||||
await this.registrationReady;
|
||||
}
|
||||
|
||||
_setCheckedElements(value, check) {
|
||||
for (let i = 0; i < this.formElements.length; i += 1) {
|
||||
if (this.multipleChoice) {
|
||||
this.formElements[i].checked = value.includes(this.formElements[i].value);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,14 @@ export const FormGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
set modelValue(values) {
|
||||
if (this.__isInitialModelValue) {
|
||||
this.__isInitialModelValue = false;
|
||||
this.registrationComplete.then(() => {
|
||||
this._setValueMapForAllFormElements('modelValue', values);
|
||||
});
|
||||
} else {
|
||||
this._setValueMapForAllFormElements('modelValue', values);
|
||||
}
|
||||
}
|
||||
|
||||
get serializedValue() {
|
||||
|
|
@ -84,7 +91,14 @@ export const FormGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
set serializedValue(values) {
|
||||
if (this.__isInitialSerializedValue) {
|
||||
this.__isInitialSerializedValue = false;
|
||||
this.registrationComplete.then(() => {
|
||||
this._setValueMapForAllFormElements('serializedValue', values);
|
||||
});
|
||||
} else {
|
||||
this._setValueMapForAllFormElements('serializedValue', values);
|
||||
}
|
||||
}
|
||||
|
||||
get formattedValue() {
|
||||
|
|
@ -107,6 +121,8 @@ export const FormGroupMixin = dedupeMixin(
|
|||
this.touched = false;
|
||||
this.focused = false;
|
||||
this.__addedSubValidators = false;
|
||||
this.__isInitialModelValue = true;
|
||||
this.__isInitialSerializedValue = true;
|
||||
|
||||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this);
|
||||
|
||||
|
|
@ -116,28 +132,56 @@ export const FormGroupMixin = dedupeMixin(
|
|||
this.addEventListener('validate-performed', this.__onChildValidatePerformed);
|
||||
|
||||
this.defaultValidators = [new FormElementsHaveNoError()];
|
||||
|
||||
this.registrationComplete = new Promise((resolve, reject) => {
|
||||
this.__resolveRegistrationComplete = resolve;
|
||||
this.__rejectRegistrationComplete = reject;
|
||||
});
|
||||
this.registrationComplete.done = false;
|
||||
this.registrationComplete.then(
|
||||
() => {
|
||||
this.registrationComplete.done = true;
|
||||
},
|
||||
() => {
|
||||
this.registrationComplete.done = true;
|
||||
throw new Error(
|
||||
'Registration could not finish. Please use await el.registrationComplete;',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// eslint-disable-next-line wc/guard-super-call
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'group');
|
||||
|
||||
this.__registrationCompleteTimer = setTimeout(() => {
|
||||
this.__resolveRegistrationComplete();
|
||||
});
|
||||
|
||||
this.registrationComplete.then(() => {
|
||||
this.__isInitialModelValue = false;
|
||||
this.__isInitialSerializedValue = false;
|
||||
this.__initInteractionStates();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback(); // eslint-disable-line wc/guard-super-call
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
if (this.__hasActiveOutsideClickHandling) {
|
||||
document.removeEventListener('click', this._checkForOutsideClick);
|
||||
this.__hasActiveOutsideClickHandling = false;
|
||||
}
|
||||
clearTimeout(this.__registrationCompleteTimer);
|
||||
if (this.registrationComplete.done === false) {
|
||||
this.__rejectRegistrationComplete();
|
||||
}
|
||||
}
|
||||
|
||||
async __initInteractionStates() {
|
||||
if (!this.registrationHasCompleted) {
|
||||
await this.registrationComplete;
|
||||
}
|
||||
__initInteractionStates() {
|
||||
this.formElements.forEach(el => {
|
||||
if (typeof el.initInteractionState === 'function') {
|
||||
el.initInteractionState();
|
||||
|
|
@ -145,6 +189,15 @@ export const FormGroupMixin = dedupeMixin(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override from FormControlMixin
|
||||
*/
|
||||
_triggerInitialModelValueChangedEvent() {
|
||||
this.registrationComplete.then(() => {
|
||||
this.__dispatchInitialModelValueChangedEvent();
|
||||
});
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
|
|
@ -269,20 +322,13 @@ export const FormGroupMixin = dedupeMixin(
|
|||
return result;
|
||||
}
|
||||
|
||||
async _setValueForAllFormElements(property, value) {
|
||||
if (!this.__readyForRegistration) {
|
||||
await this.registrationReady;
|
||||
}
|
||||
_setValueForAllFormElements(property, value) {
|
||||
this.formElements.forEach(el => {
|
||||
el[property] = value; // eslint-disable-line no-param-reassign
|
||||
});
|
||||
}
|
||||
|
||||
async _setValueMapForAllFormElements(property, values) {
|
||||
if (!this.__readyForRegistration) {
|
||||
await this.registrationReady;
|
||||
}
|
||||
|
||||
_setValueMapForAllFormElements(property, values) {
|
||||
if (values && typeof values === 'object') {
|
||||
Object.keys(values).forEach(name => {
|
||||
if (Array.isArray(this.formElements[name])) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { dedupeMixin } from '@lion/core';
|
||||
import { formRegistrarManager } from './formRegistrarManager.js';
|
||||
|
||||
/**
|
||||
* #FormRegisteringMixin:
|
||||
|
|
@ -13,50 +12,22 @@ export const FormRegisteringMixin = dedupeMixin(
|
|||
superclass =>
|
||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||
class FormRegisteringMixin extends superclass {
|
||||
constructor() {
|
||||
super();
|
||||
this.__boundDispatchRegistration = this._dispatchRegistration.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
this.__setupRegistrationHook();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
this._unregisterFormElement();
|
||||
}
|
||||
|
||||
__setupRegistrationHook() {
|
||||
if (formRegistrarManager.ready) {
|
||||
this._dispatchRegistration();
|
||||
} else {
|
||||
formRegistrarManager.addEventListener(
|
||||
'all-forms-open-for-registration',
|
||||
this.__boundDispatchRegistration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_dispatchRegistration() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('form-element-register', {
|
||||
detail: { element: this },
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
formRegistrarManager.removeEventListener(
|
||||
'all-forms-open-for-registration',
|
||||
this.__boundDispatchRegistration,
|
||||
);
|
||||
}
|
||||
|
||||
_unregisterFormElement() {
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
if (this.__parentFormGroup) {
|
||||
this.__parentFormGroup.removeFormElement(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// eslint-disable-next-line max-classes-per-file
|
||||
import { dedupeMixin } from '@lion/core';
|
||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||
import { formRegistrarManager } from './formRegistrarManager.js';
|
||||
import { FormControlsCollection } from './FormControlsCollection.js';
|
||||
|
||||
// TODO: rename .formElements to .formControls? (or .$controls ?)
|
||||
|
|
@ -31,7 +30,7 @@ export const FormRegistrarMixin = dedupeMixin(
|
|||
* (multi)select)
|
||||
* @type {boolean}
|
||||
*/
|
||||
_isFormOrFieldset: Boolean,
|
||||
_isFormOrFieldset: { type: Boolean },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -41,58 +40,14 @@ export const FormRegistrarMixin = dedupeMixin(
|
|||
|
||||
this._isFormOrFieldset = false;
|
||||
|
||||
this.__readyForRegistration = false;
|
||||
this.__hasBeenRendered = false;
|
||||
this.registrationReady = new Promise(resolve => {
|
||||
this.__resolveRegistrationReady = resolve;
|
||||
});
|
||||
this.registrationComplete = new Promise(resolve => {
|
||||
this.__resolveRegistrationComplete = resolve;
|
||||
});
|
||||
|
||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
formRegistrarManager.add(this);
|
||||
if (this.__hasBeenRendered) {
|
||||
formRegistrarManager.becomesReady();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
formRegistrarManager.remove(this);
|
||||
}
|
||||
|
||||
isRegisteredFormElement(el) {
|
||||
return this.formElements.some(exitingEl => exitingEl === el);
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__resolveRegistrationReady();
|
||||
this.__readyForRegistration = true;
|
||||
|
||||
// After we allow our children to register, we need to wait one tick before they
|
||||
// all sent their 'form-element-register' event.
|
||||
// TODO: allow developer to delay this moment, similar to LitElement.performUpdate can be
|
||||
// delayed.
|
||||
setTimeout(() => {
|
||||
this.registrationHasCompleted = true;
|
||||
this.__resolveRegistrationComplete();
|
||||
});
|
||||
|
||||
formRegistrarManager.becomesReady();
|
||||
this.__hasBeenRendered = true;
|
||||
}
|
||||
|
||||
addFormElement(child, indexToInsertAt) {
|
||||
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { dedupeMixin } from '@lion/core';
|
||||
import { formRegistrarManager } from './formRegistrarManager.js';
|
||||
|
||||
/**
|
||||
* This allows to register fields within a form even though they are not within the same dom tree.
|
||||
|
|
@ -19,64 +18,27 @@ export const FormRegistrarPortalMixin = dedupeMixin(
|
|||
class FormRegistrarPortalMixin extends superclass {
|
||||
constructor() {
|
||||
super();
|
||||
this.formElements = [];
|
||||
this.registrationTarget = undefined;
|
||||
this.__hasBeenRendered = false;
|
||||
this.__readyForRegistration = false;
|
||||
this.registrationReady = new Promise(resolve => {
|
||||
this.__resolveRegistrationReady = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
formRegistrarManager.add(this);
|
||||
if (this.__hasBeenRendered) {
|
||||
formRegistrarManager.becomesReady();
|
||||
}
|
||||
|
||||
this.__redispatchEventForFormRegistrarPortalMixin = ev => {
|
||||
ev.stopPropagation();
|
||||
this.registrationTarget.dispatchEvent(
|
||||
new CustomEvent('form-element-register', {
|
||||
detail: { element: ev.detail.element },
|
||||
bubbles: true,
|
||||
}),
|
||||
this.__redispatchEventForFormRegistrarPortalMixin = this.__redispatchEventForFormRegistrarPortalMixin.bind(
|
||||
this,
|
||||
);
|
||||
};
|
||||
this.addEventListener(
|
||||
'form-element-register',
|
||||
this.__redispatchEventForFormRegistrarPortalMixin,
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
formRegistrarManager.remove(this);
|
||||
this.removeEventListener(
|
||||
'form-element-register',
|
||||
this.__redispatchEventForFormRegistrarPortalMixin,
|
||||
);
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
this.__checkRegistrationTarget();
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__resolveRegistrationReady();
|
||||
this.__readyForRegistration = true;
|
||||
formRegistrarManager.becomesReady(this);
|
||||
this.__hasBeenRendered = true;
|
||||
}
|
||||
|
||||
__checkRegistrationTarget() {
|
||||
__redispatchEventForFormRegistrarPortalMixin(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.registrationTarget) {
|
||||
throw new Error('A FormRegistrarPortal element requires a .registrationTarget');
|
||||
}
|
||||
this.registrationTarget.dispatchEvent(
|
||||
new CustomEvent('form-element-register', {
|
||||
detail: { element: ev.detail.element },
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* Allows to align the timing for all Registrars (like form, fieldset).
|
||||
* e.g. it will only be ready once all Registrars have been fully rendered
|
||||
*
|
||||
* This is a requirement for ShadyDOM as otherwise forms can not catch registration events
|
||||
*/
|
||||
class FormRegistrarManager {
|
||||
constructor() {
|
||||
this.__elements = [];
|
||||
this._fakeExtendsEventTarget();
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
add(registrar) {
|
||||
this.__elements.push(registrar);
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
remove(registrar) {
|
||||
this.__elements.splice(this.__elements.indexOf(registrar), 1);
|
||||
}
|
||||
|
||||
becomesReady() {
|
||||
if (this.__elements.every(el => el.__readyForRegistration === true)) {
|
||||
this.dispatchEvent(new Event('all-forms-open-for-registration'));
|
||||
this.ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
_fakeExtendsEventTarget() {
|
||||
const delegate = document.createDocumentFragment();
|
||||
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => {
|
||||
this[funcName] = (...args) => delegate[funcName](...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const formRegistrarManager = new FormRegistrarManager();
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export { formFixture } from './test-helpers/formFixture.js';
|
||||
export {
|
||||
AlwaysInvalid,
|
||||
AlwaysValid,
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { fixture, aTimeout } from '@open-wc/testing';
|
||||
|
||||
export async function formFixture(...args) {
|
||||
const el = await fixture(...args);
|
||||
if (el.registrationComplete) {
|
||||
await el.registrationComplete;
|
||||
await aTimeout();
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { FormRegisteringMixin } from '../src/registration/FormRegisteringMixin.js';
|
||||
import { formRegistrarManager } from '../src/registration/formRegistrarManager.js';
|
||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||
import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPortalMixin.js';
|
||||
|
||||
|
|
@ -12,11 +10,10 @@ export const runRegistrationSuite = customConfig => {
|
|||
...customConfig,
|
||||
};
|
||||
|
||||
describe('FormRegistrationMixins', () => {
|
||||
describe(`FormRegistrationMixins ${cfg.suffix}`, () => {
|
||||
let parentTag;
|
||||
let childTag;
|
||||
let portalTag;
|
||||
let portalTagString;
|
||||
|
||||
before(async () => {
|
||||
if (!cfg.parentTagString) {
|
||||
|
|
@ -30,7 +27,6 @@ export const runRegistrationSuite = customConfig => {
|
|||
}
|
||||
|
||||
parentTag = unsafeStatic(cfg.parentTagString);
|
||||
portalTagString = cfg.portalTagString;
|
||||
childTag = unsafeStatic(cfg.childTagString);
|
||||
portalTag = unsafeStatic(cfg.portalTagString);
|
||||
});
|
||||
|
|
@ -41,44 +37,33 @@ export const runRegistrationSuite = customConfig => {
|
|||
<${childTag}></${childTag}>
|
||||
</${parentTag}>
|
||||
`);
|
||||
await el.registrationReady;
|
||||
expect(el.formElements.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('can register a formElement with arbitrary dom tree in between registrar and registering', async () => {
|
||||
const el = await fixture(html`
|
||||
<${parentTag}>
|
||||
<div>
|
||||
<${childTag}></${childTag}>
|
||||
</div>
|
||||
</${parentTag}>
|
||||
`);
|
||||
expect(el.formElements.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('supports nested registration parents', async () => {
|
||||
const el = await fixture(html`
|
||||
<${parentTag}>
|
||||
<${parentTag}>
|
||||
<${parentTag} class="sub-group">
|
||||
<${childTag}></${childTag}>
|
||||
<${childTag}></${childTag}>
|
||||
</${parentTag}>
|
||||
</${parentTag}>
|
||||
`);
|
||||
await el.registrationReady;
|
||||
expect(el.formElements.length).to.equal(1);
|
||||
expect(el.querySelector(cfg.parentTagString).formElements.length).to.equal(2);
|
||||
});
|
||||
|
||||
it('forgets disconnected registrars', async () => {
|
||||
const el = await fixture(html`
|
||||
<${parentTag}>
|
||||
<${parentTag}>
|
||||
<${childTag}</${childTag}
|
||||
</${parentTag}>
|
||||
</${parentTag}>
|
||||
`);
|
||||
|
||||
const secondRegistrar = await fixture(html`
|
||||
<${parentTag}>
|
||||
<${childTag}</${childTag}
|
||||
</${parentTag}>
|
||||
`);
|
||||
|
||||
el.appendChild(secondRegistrar);
|
||||
expect(formRegistrarManager.__elements.length).to.equal(3);
|
||||
|
||||
el.removeChild(secondRegistrar);
|
||||
expect(formRegistrarManager.__elements.length).to.equal(2);
|
||||
const subGroup = el.querySelector('.sub-group');
|
||||
expect(subGroup.formElements.length).to.equal(2);
|
||||
});
|
||||
|
||||
it('works for components that have a delayed render', async () => {
|
||||
|
|
@ -100,7 +85,6 @@ export const runRegistrationSuite = customConfig => {
|
|||
<${childTag}></${childTag}>
|
||||
</${tagWrapper}>
|
||||
`);
|
||||
await el.registrationReady;
|
||||
expect(el.formElements.length).to.equal(1);
|
||||
});
|
||||
|
||||
|
|
@ -113,7 +97,6 @@ export const runRegistrationSuite = customConfig => {
|
|||
const newField = await fixture(html`
|
||||
<${childTag}></${childTag}>
|
||||
`);
|
||||
|
||||
expect(el.formElements.length).to.equal(1);
|
||||
|
||||
el.appendChild(newField);
|
||||
|
|
@ -136,22 +119,14 @@ export const runRegistrationSuite = customConfig => {
|
|||
`);
|
||||
newField.myProp = 'test';
|
||||
|
||||
el.children[1].insertAdjacentElement('beforebegin', newField);
|
||||
el.insertBefore(newField, el.children[1]);
|
||||
|
||||
expect(el.formElements.length).to.equal(4);
|
||||
expect(el.children[1].myProp).to.equal('test');
|
||||
expect(el.formElements[1].myProp).to.equal('test');
|
||||
});
|
||||
|
||||
describe('FormRegistrarPortalMixin', () => {
|
||||
it('throws if there is no .registrationTarget', async () => {
|
||||
// we test the private api directly as errors thrown from a web component are in a
|
||||
// different context and we can not catch them here
|
||||
const el = document.createElement(portalTagString);
|
||||
expect(() => {
|
||||
el.__checkRegistrationTarget();
|
||||
}).to.throw('A FormRegistrarPortal element requires a .registrationTarget');
|
||||
});
|
||||
|
||||
it('forwards registrations to the .registrationTarget', async () => {
|
||||
const el = await fixture(html`<${parentTag}></${parentTag}>`);
|
||||
await fixture(html`
|
||||
|
|
@ -221,21 +196,6 @@ export const runRegistrationSuite = customConfig => {
|
|||
expect(el.formElements[5]).dom.to.equal(anotherField);
|
||||
});
|
||||
|
||||
// find a proper way to do this on polyfilled browsers
|
||||
it.skip('fires event "form-element-register" with the child as ev.target', async () => {
|
||||
const registerSpy = sinon.spy();
|
||||
const el = await fixture(
|
||||
html`<${parentTag} @form-element-register=${registerSpy}></${parentTag}>`,
|
||||
);
|
||||
const portal = await fixture(html`
|
||||
<${portalTag} .registrationTarget=${el}>
|
||||
<${childTag}></${childTag}>
|
||||
</${portalTag}>
|
||||
`);
|
||||
const childEl = portal.children[0];
|
||||
expect(registerSpy.args[2][0].target.tagName).to.equal(childEl.tagName);
|
||||
});
|
||||
|
||||
it('keeps working if moving the portal itself', async () => {
|
||||
const el = await fixture(html`<${parentTag}></${parentTag}>`);
|
||||
const portal = await fixture(html`
|
||||
|
|
@ -280,7 +240,6 @@ export const runRegistrationSuite = customConfig => {
|
|||
</${delayedPortalTag}>
|
||||
`);
|
||||
|
||||
await el.registrationReady;
|
||||
expect(el.formElements.length).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -240,7 +240,11 @@ export function runValidateMixinFeedbackPart() {
|
|||
}
|
||||
|
||||
render() {
|
||||
return html`Custom for ${this.feedbackData[0].validator.constructor.validatorName}`;
|
||||
const name =
|
||||
this.feedbackData && this.feedbackData[0]
|
||||
? this.feedbackData[0].validator.constructor.validatorName
|
||||
: '';
|
||||
return html`Custom for ${name}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { expect, html, defineCE, unsafeStatic } from '@open-wc/testing';
|
||||
import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing';
|
||||
import { LitElement, SlotMixin } from '@lion/core';
|
||||
import sinon from 'sinon';
|
||||
import { formFixture as fixture } from '../test-helpers/formFixture.js';
|
||||
import { FormControlMixin } from '../src/FormControlMixin.js';
|
||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||
|
||||
|
|
@ -219,6 +218,8 @@ describe('FormControlMixin', () => {
|
|||
</${groupTag}>
|
||||
`);
|
||||
const fieldsetEl = formEl.querySelector('[name=fieldset]');
|
||||
await formEl.registrationComplete;
|
||||
await fieldsetEl.registrationComplete;
|
||||
|
||||
expect(fieldsetSpy.callCount).to.equal(1);
|
||||
const fieldsetEv = fieldsetSpy.firstCall.args[0];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { html, LitElement } from '@lion/core';
|
||||
import { formFixture as fixture } from '@lion/form-core/test-helpers.js';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import { LionInput } from '@lion/input';
|
||||
import { FormGroupMixin, Required } from '@lion/form-core';
|
||||
import { expect, nextFrame } from '@open-wc/testing';
|
||||
import { expect, fixture } from '@open-wc/testing';
|
||||
import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js';
|
||||
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
||||
|
||||
|
|
@ -32,7 +31,6 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
expect(el.modelValue).to.equal('female');
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.modelValue).to.equal('male');
|
||||
|
|
@ -48,7 +46,6 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
const invalidChild = await fixture(html`
|
||||
<choice-group-input .modelValue=${'Lara'}></choice-group-input>
|
||||
`);
|
||||
|
|
@ -67,7 +64,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
await el.registrationComplete;
|
||||
|
||||
expect(el.formElements[0].name).to.equal('gender');
|
||||
expect(el.formElements[1].name).to.equal('gender');
|
||||
|
|
@ -87,7 +84,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
const invalidChild = await fixture(html`
|
||||
<choice-group-input name="foo" .choiceValue=${'male'}></choice-group-input>
|
||||
`);
|
||||
|
|
@ -107,10 +104,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
|
||||
await nextFrame();
|
||||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
await el.registrationComplete;
|
||||
|
||||
expect(el.modelValue).to.equal('other');
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
|
|
@ -124,8 +118,10 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
|
||||
expect(el.serializedValue).to.equal('other');
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
it('can handle complex data via choiceValue', async () => {
|
||||
|
|
@ -137,7 +133,6 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${date} checked></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
expect(el.modelValue).to.equal(date);
|
||||
el.formElements[0].checked = true;
|
||||
|
|
@ -151,7 +146,6 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${''}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
expect(el.modelValue).to.equal(0);
|
||||
el.formElements[1].checked = true;
|
||||
|
|
@ -170,7 +164,8 @@ describe('ChoiceGroupMixin', () => {
|
|||
></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
await el.registrationComplete;
|
||||
|
||||
expect(el.modelValue).to.equal('female');
|
||||
el.modelValue = 'other';
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
|
|
@ -190,7 +185,8 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
await el.registrationComplete;
|
||||
|
||||
counter = 0; // reset after setup which may result in different results
|
||||
|
||||
el.formElements[0].checked = true;
|
||||
|
|
@ -253,7 +249,6 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'female'}></choice-group-input>
|
||||
</choice-group>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
expect(el.serializedValue).to.deep.equal('');
|
||||
});
|
||||
|
|
@ -267,7 +262,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
<choice-group-input .choiceValue=${'other'}></choice-group-input>
|
||||
</choice-group-multiple>
|
||||
`);
|
||||
await nextFrame();
|
||||
|
||||
expect(el.modelValue).to.eql(['female']);
|
||||
el.formElements[0].checked = true;
|
||||
expect(el.modelValue).to.eql(['male', 'female']);
|
||||
|
|
@ -284,9 +279,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
</choice-group-multiple>
|
||||
`);
|
||||
|
||||
await nextFrame();
|
||||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
await el.registrationComplete;
|
||||
el.modelValue = ['male', 'other'];
|
||||
expect(el.modelValue).to.eql(['male', 'other']);
|
||||
expect(el.formElements[0].checked).to.be.true;
|
||||
|
|
@ -302,9 +295,7 @@ describe('ChoiceGroupMixin', () => {
|
|||
</choice-group-multiple>
|
||||
`);
|
||||
|
||||
await nextFrame();
|
||||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
await el.registrationComplete;
|
||||
expect(el.modelValue).to.eql(['male', 'other']);
|
||||
expect(el.formElements[0].checked).to.be.true;
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
|
|
@ -328,8 +319,6 @@ describe('ChoiceGroupMixin', () => {
|
|||
</lion-fieldset>
|
||||
`);
|
||||
|
||||
await nextFrame();
|
||||
await el.registrationReady;
|
||||
await el.updateComplete;
|
||||
expect(el.serializedValue).to.eql({
|
||||
gender: 'female',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, html, unsafeStatic } from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/form-core/test-helpers.js';
|
||||
import { expect, html, unsafeStatic, fixture } from '@open-wc/testing';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import sinon from 'sinon';
|
||||
|
|
@ -82,13 +81,14 @@ const choiceGroupDispatchesCountOnFirstPaint = (groupTagname, itemTagname, count
|
|||
const itemTag = unsafeStatic(itemTagname);
|
||||
it(getFirstPaintTitle(count), async () => {
|
||||
const spy = sinon.spy();
|
||||
await fixture(html`
|
||||
const el = await fixture(html`
|
||||
<${groupTag} @model-value-changed="${spy}">
|
||||
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
||||
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
||||
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
||||
</${groupTag}>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
expect(spy.callCount).to.equal(count);
|
||||
});
|
||||
};
|
||||
|
|
@ -105,6 +105,7 @@ const choiceGroupDispatchesCountOnInteraction = (groupTagname, itemTagname, coun
|
|||
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
||||
</${groupTag}>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
const option2 = el.querySelector(`${itemTagname}:nth-child(2)`);
|
||||
option2.checked = true;
|
||||
|
|
@ -209,7 +210,7 @@ describe('lion-select-rich', () => {
|
|||
describe(featureName, () => {
|
||||
it(getFirstPaintTitle(firstStampCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
await fixture(html`
|
||||
const el = await fixture(html`
|
||||
<lion-select-rich @model-value-changed="${spy}">
|
||||
<lion-options slot="input">
|
||||
<lion-option .choiceValue="${'option1'}"></lion-option>
|
||||
|
|
@ -218,6 +219,7 @@ describe('lion-select-rich', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
expect(spy.callCount).to.equal(firstStampCount);
|
||||
});
|
||||
|
||||
|
|
@ -232,6 +234,7 @@ describe('lion-select-rich', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
const option2 = el.querySelector('lion-option:nth-child(2)');
|
||||
option2.checked = true;
|
||||
|
|
@ -250,11 +253,12 @@ describe('lion-fieldset', () => {
|
|||
describe(featureName, () => {
|
||||
it(getFirstPaintTitle(firstStampCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
await fixture(html`
|
||||
const el = await fixture(html`
|
||||
<lion-fieldset name="parent" @model-value-changed="${spy}">
|
||||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
expect(spy.callCount).to.equal(firstStampCount);
|
||||
});
|
||||
|
||||
|
|
@ -265,6 +269,7 @@ describe('lion-fieldset', () => {
|
|||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
const input = el.querySelector('lion-input');
|
||||
input.modelValue = 'foo';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { formFixture as fixture } from '@lion/form-core/test-helpers.js';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { expect, html } from '@open-wc/testing';
|
||||
import { expect, html, fixture } from '@open-wc/testing';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import sinon from 'sinon';
|
||||
|
||||
|
|
@ -32,6 +31,7 @@ describe('model value event', () => {
|
|||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
await fieldset.registrationComplete;
|
||||
fieldset.addEventListener('model-value-changed', spy);
|
||||
const input = fieldset.querySelector('lion-input');
|
||||
input.modelValue = 'foo';
|
||||
|
|
@ -50,6 +50,9 @@ describe('model value event', () => {
|
|||
`);
|
||||
const parent = grandparent.querySelector('[name=parent]');
|
||||
const input = grandparent.querySelector('[name=input]');
|
||||
await grandparent.registrationComplete;
|
||||
await parent.registrationComplete;
|
||||
|
||||
grandparent.addEventListener('model-value-changed', spy);
|
||||
input.modelValue = 'foo';
|
||||
const e = spy.firstCall.args[0];
|
||||
|
|
@ -73,6 +76,9 @@ describe('model value event', () => {
|
|||
`);
|
||||
const parent = grandparent.querySelector('[name=parent]');
|
||||
const input = grandparent.querySelector('[name=input]');
|
||||
await grandparent.registrationComplete;
|
||||
await parent.registrationComplete;
|
||||
|
||||
grandparent.addEventListener('model-value-changed', spy);
|
||||
input.modelValue = 'foo';
|
||||
const e = spy.firstCall.args[0];
|
||||
|
|
@ -115,6 +121,9 @@ describe('model value event', () => {
|
|||
`);
|
||||
const parent = grandparent.querySelector('[name="parent"]');
|
||||
const input = grandparent.querySelector('[name="input"]');
|
||||
await grandparent.registrationComplete;
|
||||
await parent.registrationComplete;
|
||||
|
||||
const spies = [];
|
||||
[grandparent, parent, input].forEach(element => {
|
||||
const spy = sinon.spy();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('<lion-input>', () => {
|
|||
|
||||
it('delegates value attribute', async () => {
|
||||
const el = await fixture(html`<${tag} value="prefilled"></${tag}>`);
|
||||
expect(el._inputNode.value).to.equal('prefilled');
|
||||
expect(el._inputNode.getAttribute('value')).to.equal('prefilled');
|
||||
});
|
||||
|
||||
it('automatically creates an <input> element if not provided by user', async () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from '@lion/form-core';
|
||||
import { css, html, LitElement, ScopedElementsMixin, SlotMixin } from '@lion/core';
|
||||
|
||||
import { formRegistrarManager } from '@lion/form-core/src/registration/formRegistrarManager.js';
|
||||
import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
|
||||
import './differentKeyNamesShimIE.js';
|
||||
import { LionSelectInvoker } from './LionSelectInvoker.js';
|
||||
|
|
@ -137,24 +136,6 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
return this._listboxNode.querySelector(`#${this._listboxActiveDescendant}`);
|
||||
}
|
||||
|
||||
get modelValue() {
|
||||
const el = this.formElements.find(option => option.checked);
|
||||
return el ? el.modelValue.value : '';
|
||||
}
|
||||
|
||||
set modelValue(value) {
|
||||
const el = this.formElements.find(option => option.modelValue.value === value);
|
||||
|
||||
if (el) {
|
||||
el.checked = true;
|
||||
} else {
|
||||
// cache user set modelValue, and then try it again when registration is done
|
||||
this.__cachedUserSetModelValue = value;
|
||||
}
|
||||
|
||||
this.requestUpdate('modelValue');
|
||||
}
|
||||
|
||||
get serializedValue() {
|
||||
return this.modelValue;
|
||||
}
|
||||
|
|
@ -209,30 +190,27 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
this.__hasInitialSelectedFormElement = false;
|
||||
this.hasNoDefaultSelected = false;
|
||||
this._repropagationRole = 'choice-group'; // configures FormControlMixin
|
||||
this.__initInteractionStates();
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
this._listboxNode.registrationTarget = this;
|
||||
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
|
||||
this.__setupInvokerNode();
|
||||
this.__setupListboxNode();
|
||||
this.__setupEventListeners();
|
||||
|
||||
this.__toggleInvokerDisabled();
|
||||
|
||||
this.registrationComplete.then(() => {
|
||||
this.__initInteractionStates();
|
||||
});
|
||||
|
||||
this._overlaySetupComplete.then(() => {
|
||||
this.__setupOverlay();
|
||||
});
|
||||
|
||||
this.__setupInvokerNode();
|
||||
this.__setupListboxNode();
|
||||
this.__setupEventListeners();
|
||||
this._listboxNode.registrationTarget = this;
|
||||
|
||||
formRegistrarManager.addEventListener('all-forms-open-for-registration', () => {
|
||||
// Now that we have rendered + registered our listbox, try setting the user defined modelValue again
|
||||
if (this.__cachedUserSetModelValue) {
|
||||
this.modelValue = this.__cachedUserSetModelValue;
|
||||
}
|
||||
});
|
||||
|
||||
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
|
||||
this.__toggleInvokerDisabled();
|
||||
}
|
||||
|
||||
_requestUpdate(name, oldValue) {
|
||||
|
|
@ -248,13 +226,18 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
}
|
||||
}
|
||||
|
||||
async __initInteractionStates() {
|
||||
await this.registrationComplete;
|
||||
// This timeout is here, so that we know we handle after the initial model-value
|
||||
// event (see firstUpdated method FormConrtolMixin) has fired.
|
||||
setTimeout(() => {
|
||||
/**
|
||||
* In the select disabled options are still going to a possible value for example
|
||||
* when prefilling or programmatically setting it.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_getCheckedElements() {
|
||||
return this.formElements.filter(el => el.checked);
|
||||
}
|
||||
|
||||
__initInteractionStates() {
|
||||
this.initInteractionState();
|
||||
});
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ describe('Select Rich Integration tests', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await nestedEl.registrationComplete;
|
||||
|
||||
await fixture(html`
|
||||
<${tag} id="main">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Required } from '@lion/form-core';
|
||||
import { expect, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/form-core/test-helpers.js';
|
||||
import { expect, html, triggerBlurFor, triggerFocusFor, fixture } from '@open-wc/testing';
|
||||
|
||||
import '../lion-option.js';
|
||||
import '../lion-options.js';
|
||||
|
|
@ -270,6 +269,7 @@ describe('lion-select-rich interactions', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
expect(el.modelValue).to.equal(10);
|
||||
});
|
||||
|
||||
|
|
@ -489,6 +489,7 @@ describe('lion-select-rich interactions', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
expect(el.dirty).to.be.false;
|
||||
el.modelValue = 20;
|
||||
expect(el.dirty).to.be.true;
|
||||
|
|
@ -541,6 +542,7 @@ describe('lion-select-rich interactions', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
|
||||
expect(el.hasFeedbackFor).to.include('error');
|
||||
expect(el.validationStates).to.have.a.property('error');
|
||||
|
|
@ -578,6 +580,7 @@ describe('lion-select-rich interactions', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('first');
|
||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||
expect(el._listboxNode.getAttribute('aria-activedescendant')).to.equal('second');
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
import { formFixture as fixture } from '@lion/form-core/test-helpers.js';
|
||||
import { OverlayController } from '@lion/overlays';
|
||||
import { Required } from '@lion/form-core';
|
||||
import { aTimeout, defineCE, expect, html, nextFrame, unsafeStatic } from '@open-wc/testing';
|
||||
import {
|
||||
aTimeout,
|
||||
defineCE,
|
||||
expect,
|
||||
html,
|
||||
nextFrame,
|
||||
unsafeStatic,
|
||||
fixture,
|
||||
} from '@open-wc/testing';
|
||||
import { LionSelectInvoker, LionSelectRich } from '../index.js';
|
||||
import '../lion-option.js';
|
||||
import '../lion-options.js';
|
||||
|
|
@ -32,7 +39,7 @@ describe('lion-select-rich', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await nextFrame();
|
||||
await el.registrationComplete;
|
||||
|
||||
expect(el.formElements[0].name).to.equal('foo');
|
||||
expect(el.formElements[1].name).to.equal('foo');
|
||||
|
|
@ -95,46 +102,39 @@ describe('lion-select-rich', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
|
||||
expect(el.modelValue).to.equal('other');
|
||||
expect(el.formElements[2].checked).to.be.true;
|
||||
});
|
||||
|
||||
it(`has a fieldName based on the label`, async () => {
|
||||
const el1 = await fixture(
|
||||
html`
|
||||
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`
|
||||
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`
|
||||
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`
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
@ -446,6 +446,7 @@ describe('lion-select-rich', () => {
|
|||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
await el.registrationComplete;
|
||||
// The default is min, so we override that behavior here
|
||||
el._overlayCtrl.inheritsReferenceWidth = 'full';
|
||||
el._initialInheritsReferenceWidth = 'full';
|
||||
|
|
@ -532,7 +533,6 @@ describe('lion-select-rich', () => {
|
|||
</lion-select-rich>
|
||||
`);
|
||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.be.false;
|
||||
});
|
||||
|
||||
|
|
@ -544,7 +544,6 @@ describe('lion-select-rich', () => {
|
|||
`);
|
||||
// tab can only be caught via keydown
|
||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.be.false;
|
||||
});
|
||||
});
|
||||
|
|
@ -584,7 +583,6 @@ describe('lion-select-rich', () => {
|
|||
</lion-select-rich>
|
||||
`);
|
||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.be.false;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
75
web-test-runner-browserstack.config.js
Normal file
75
web-test-runner-browserstack.config.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
const { legacyPlugin } = require('@web/dev-server-legacy');
|
||||
const { browserstackLauncher } = require('@web/test-runner-browserstack');
|
||||
|
||||
const sharedCapabilities = {
|
||||
// it's recommended to store user and key as environment variables
|
||||
'browserstack.user': process.env.BROWSER_STACK_USERNAME,
|
||||
'browserstack.key': process.env.BROWSER_STACK_ACCESS_KEY,
|
||||
|
||||
project: '@lion',
|
||||
name: '@lion web components',
|
||||
// if you are running tests in a CI, the build id might be available as an
|
||||
// environment variable. this is useful for identifying test runs
|
||||
// this is for example the name for github actions
|
||||
build: `build ${process.env.GITHUB_RUN_NUMBER || 'unknown'}`,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
plugins: [legacyPlugin()],
|
||||
nodeResolve: true,
|
||||
sessionStartTimeout: 60000,
|
||||
concurrency: 1,
|
||||
coverageConfig: {
|
||||
threshold: {
|
||||
statements: 80,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 80,
|
||||
},
|
||||
},
|
||||
browsers: [
|
||||
// browserstackLauncher({
|
||||
// capabilities: {
|
||||
// ...sharedCapabilities,
|
||||
// browserName: 'Chrome',
|
||||
// os: 'Windows',
|
||||
// os_version: '10',
|
||||
// },
|
||||
// }),
|
||||
// browserstackLauncher({
|
||||
// capabilities: {
|
||||
// ...sharedCapabilities,
|
||||
// browserName: 'Firefox',
|
||||
// os: 'Windows',
|
||||
// os_version: '10',
|
||||
// },
|
||||
// }),
|
||||
// browserstackLauncher({
|
||||
// capabilities: {
|
||||
// ...sharedCapabilities,
|
||||
// browserName: 'Firefox',
|
||||
// browser_version: '60.0',
|
||||
// os: 'Windows',
|
||||
// os_version: '10',
|
||||
// },
|
||||
// }),
|
||||
// browserstackLauncher({
|
||||
// capabilities: {
|
||||
// ...sharedCapabilities,
|
||||
// browserName: 'Safari',
|
||||
// browser_version: '11.1',
|
||||
// os: 'OS X',
|
||||
// os_version: 'High Sierra',
|
||||
// },
|
||||
// }),
|
||||
browserstackLauncher({
|
||||
capabilities: {
|
||||
...sharedCapabilities,
|
||||
browserName: 'IE',
|
||||
browser_version: '11.0',
|
||||
os: 'Windows',
|
||||
os_version: '7',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
const { legacyPlugin } = require('@web/dev-server-legacy');
|
||||
|
||||
module.exports = {
|
||||
plugins: [legacyPlugin()],
|
||||
nodeResolve: true,
|
||||
sessionStartTimeout: 30000,
|
||||
concurrency: 5,
|
||||
|
|
|
|||
Loading…
Reference in a new issue