diff --git a/.changeset/thirty-eagles-prove.md b/.changeset/thirty-eagles-prove.md
new file mode 100644
index 000000000..782599177
--- /dev/null
+++ b/.changeset/thirty-eagles-prove.md
@@ -0,0 +1,6 @@
+---
+'@lion/form-core': patch
+'@lion/form-integrations': patch
+---
+
+Add `clear()` interface to choiceGroups
diff --git a/packages/form-core/src/choice-group/ChoiceGroupMixin.js b/packages/form-core/src/choice-group/ChoiceGroupMixin.js
index 9b8774137..a0bcd03ea 100644
--- a/packages/form-core/src/choice-group/ChoiceGroupMixin.js
+++ b/packages/form-core/src/choice-group/ChoiceGroupMixin.js
@@ -187,6 +187,14 @@ const ChoiceGroupMixinImplementation = superclass =>
super.addFormElement(child, indexToInsertAt);
}
+ clear() {
+ if (this.multipleChoice) {
+ this.modelValue = [];
+ } else {
+ this.modelValue = '';
+ }
+ }
+
/**
* @override from FormControlMixin
* @protected
diff --git a/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js b/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js
index 8d6cb2533..9ae787d41 100644
--- a/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js
+++ b/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js
@@ -507,6 +507,23 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
}
});
+ it('can be cleared', async () => {
+ const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
+ <${parentTag} name="gender[]">
+ <${childTag} .choiceValue=${'male'}>${childTag}>
+ <${childTag} .choiceValue=${'female'}>${childTag}>
+ ${parentTag}>
+ `));
+ el.formElements[0].checked = true;
+ el.clear();
+
+ if (cfg.choiceType === 'single') {
+ expect(el.serializedValue).to.deep.equal('');
+ } else {
+ expect(el.serializedValue).to.deep.equal([]);
+ }
+ });
+
describe('multipleChoice', () => {
it('has a single modelValue representing all currently checked values', async () => {
const el = /** @type {ChoiceInputGroup} */ (await fixture(html`
diff --git a/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts b/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts
index 4808f3d06..54c0c3ee1 100644
--- a/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts
+++ b/packages/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts
@@ -28,6 +28,8 @@ export declare class ChoiceGroupHost {
addFormElement(child: FormControlHost, indexToInsertAt: number): void;
+ clear(): void;
+
protected _triggerInitialModelValueChangedEvent(): void;
_getFromAllFormElements(property: string, filterCondition: Function): void;
diff --git a/packages/form-integrations/test/dialog-integrations.js b/packages/form-integrations/test/dialog-integrations.js
deleted file mode 100644
index e661f544b..000000000
--- a/packages/form-integrations/test/dialog-integrations.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { expect, fixture, html } from '@open-wc/testing';
-import './helpers/umbrella-form.js';
-import '@lion/dialog/lion-dialog.js';
-
-/**
- * @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm
- * @typedef {import('@lion/dialog/').LionDialog} LionDialog
- */
-
-// Test umbrella form inside dialog
-describe('Form inside dialog Integrations', () => {
- it('"Successfully spawns all form components inside a dialog', async () => {
- expect(
- await fixture(html`
-
-
- `),
- ).to.not.throw();
- });
-});
diff --git a/packages/form-integrations/test/dialog-integrations.test.js b/packages/form-integrations/test/dialog-integrations.test.js
new file mode 100644
index 000000000..a46106082
--- /dev/null
+++ b/packages/form-integrations/test/dialog-integrations.test.js
@@ -0,0 +1,73 @@
+import { expect, fixture, html } from '@open-wc/testing';
+import { getAllTagNames } from './helpers/helpers.js';
+import './helpers/umbrella-form.js';
+import '@lion/dialog/define';
+
+/**
+ * @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm
+ * @typedef {import('@lion/dialog').LionDialog} LionDialog
+ * @typedef {import('@lion/form').LionForm} LionForm
+ */
+
+// Test umbrella form inside dialog
+describe('Form inside dialog Integrations', () => {
+ it('Successfully registers all form components inside a dialog', async () => {
+ const el = /** @type {LionDialog} */ await fixture(html`
+
+
+ `);
+
+ // @ts-ignore
+ const formEl = /** @type {LionForm} */ (el._overlayCtrl.contentNode._lionFormNode);
+ await formEl.registrationComplete;
+ const registeredEls = getAllTagNames(formEl);
+
+ expect(registeredEls).to.eql([
+ // [1] In a dialog, these are registered before the rest (which is in chronological dom order)
+ // Ideally, this should be the same. It would be once the platform allows to create dialogs
+ // that don't need to move content to the body
+ 'lion-checkbox-group',
+ ' lion-checkbox',
+ 'lion-switch',
+ // [2] 'the rest' (chronologically ordered registrations)
+ 'lion-fieldset',
+ ' lion-input',
+ ' lion-input',
+ 'lion-input-date',
+ 'lion-input-datepicker',
+ 'lion-textarea',
+ 'lion-input-amount',
+ 'lion-input-iban',
+ 'lion-input-email',
+ 'lion-checkbox-group',
+ ' lion-checkbox',
+ ' lion-checkbox',
+ ' lion-checkbox',
+ 'lion-radio-group',
+ ' lion-radio',
+ ' lion-radio',
+ ' lion-radio',
+ 'lion-listbox',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ 'lion-combobox',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ 'lion-select-rich',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ 'lion-select',
+ 'lion-input-range',
+ // [3] this is where [1] should have been inserted
+ // [4] more of 'the rest' (chronologically ordered registrations)
+ 'lion-input-stepper',
+ 'lion-textarea',
+ ]);
+ });
+});
diff --git a/packages/form-integrations/test/form-group-methods.test.js b/packages/form-integrations/test/form-group-methods.test.js
new file mode 100644
index 000000000..86020e0af
--- /dev/null
+++ b/packages/form-integrations/test/form-group-methods.test.js
@@ -0,0 +1,161 @@
+import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
+import './helpers/umbrella-form.js';
+import { getAllFieldsAndFormGroups } from './helpers/helpers.js';
+
+/**
+ * @typedef {import('@lion/form-core').LionField} LionField
+ * @typedef {import('@lion/button').LionButton} LionButton
+ * @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm
+ */
+
+const fullyPrefilledSerializedValue = {
+ full_name: { first_name: 'Lorem', last_name: 'Ipsum' },
+ date: '2000-12-12',
+ datepicker: '2020-12-12',
+ bio: 'Lorem',
+ money: '12313.12',
+ iban: '123456',
+ email: 'a@b.com',
+ checkers: ['foo', 'bar'],
+ dinosaurs: 'brontosaurus',
+ favoriteFruit: 'Banana',
+ favoriteMovie: 'Rocky',
+ favoriteColor: 'hotpink',
+ lyrics: '1',
+ notifications: {
+ checked: true,
+ value: 'Lorem',
+ },
+ range: 2.3,
+ rsvp: 'Lorem',
+ terms: ['agreed'],
+ comments: 'Lorem',
+};
+
+const fullyChangedSerializedValue = {
+ full_name: { first_name: 'LoremChanged', last_name: 'IpsumChanged' },
+ date: '1999-12-12',
+ datepicker: '1986-12-12',
+ bio: 'LoremChanged',
+ money: '9912313.12',
+ iban: '99123456',
+ email: 'aChanged@b.com',
+ checkers: ['foo'],
+ dinosaurs: '',
+ favoriteFruit: '',
+ favoriteMovie: '',
+ favoriteColor: '',
+ lyrics: '2',
+ notifications: {
+ checked: false,
+ value: 'Lorem',
+ },
+ range: 3.3,
+ rsvp: 'LoremChanged',
+ terms: [],
+ comments: 'LoremChanged',
+};
+
+describe(`Submitting/Resetting/Clearing Form`, async () => {
+ it('pressing submit button of a form should make submitted true for all fields', async () => {
+ const el = /** @type {UmbrellaForm} */ (await fixture(html``));
+ await el.updateComplete;
+ const formEl = el._lionFormNode;
+
+ const allElements = getAllFieldsAndFormGroups(formEl);
+
+ allElements.forEach((/** @type {LionField} */ field) => {
+ if (field.tagName === 'LION-SWITCH') {
+ // TODO: remove this when this is fixed: https://github.com/ing-bank/lion/issues/1204
+ return;
+ }
+ // TODO: prefer submitted 'false' over 'undefined'
+ expect(Boolean(field.submitted)).to.be.false;
+ });
+ /** @type {LionButton} */ (formEl.querySelector('#submit_button')).click();
+ await elementUpdated(formEl);
+ await el.updateComplete;
+ allElements.forEach((/** @type {LionField} */ field) => {
+ expect(field.submitted).to.be.true;
+ });
+ });
+
+ it('calling resetGroup() should reset all metadata (interaction states and initial values)', async () => {
+ const el = /** @type {UmbrellaForm} */ (await fixture(
+ html``,
+ ));
+ await el.updateComplete;
+ const formEl = el._lionFormNode;
+
+ /** @type {LionButton} */ (formEl.querySelector('#submit_button')).click();
+ await elementUpdated(formEl);
+ await formEl.updateComplete;
+
+ const allElements = getAllFieldsAndFormGroups(formEl);
+
+ allElements.forEach((/** @type {LionField} */ field) => {
+ // eslint-disable-next-line no-param-reassign
+ field.touched = true;
+ // eslint-disable-next-line no-param-reassign
+ field.dirty = true;
+ });
+
+ formEl.serializedValue = fullyChangedSerializedValue;
+
+ allElements.forEach((/** @type {LionField} */ field) => {
+ expect(field.submitted).to.be.true;
+ expect(field.touched).to.be.true;
+ expect(field.dirty).to.be.true;
+ });
+
+ /** @type {LionButton} */ (formEl.querySelector('#reset_button')).click();
+ await elementUpdated(formEl);
+ await formEl.updateComplete;
+ expect(formEl.submitted).to.be.false;
+
+ allElements.forEach((/** @type {LionField} */ field) => {
+ expect(field.submitted).to.be.false;
+ expect(field.touched).to.be.false;
+ expect(field.dirty).to.be.false;
+ });
+
+ // TODO: investigate why this doesn't work
+ // expect(formEl.serializedValue).to.eql(fullyPrefilledSerializedValue);
+ });
+
+ // Wait till ListboxMixin properly clears
+ it('calling clearGroup() should clear all fields', async () => {
+ const el = /** @type {UmbrellaForm} */ (await fixture(
+ html``,
+ ));
+ await el.updateComplete;
+ const formEl = el._lionFormNode;
+
+ formEl.clearGroup();
+ await elementUpdated(formEl);
+ await formEl.updateComplete;
+ expect(formEl.serializedValue).to.eql({
+ full_name: { first_name: '', last_name: '' },
+ date: '',
+ datepicker: '',
+ bio: '',
+ money: '',
+ iban: '',
+ email: '',
+ checkers: [],
+ dinosaurs: '',
+ favoriteFruit: '',
+ favoriteMovie: '',
+ favoriteColor: '',
+ lyrics: '',
+ notifications: {
+ checked: false,
+ value: 'Lorem',
+ },
+ range: '',
+ rsvp: '',
+ terms: [],
+ comments: '',
+ });
+ });
+});
diff --git a/packages/form-integrations/test/form-integrations.test.js b/packages/form-integrations/test/form-integrations.test.js
index 2780cdb21..bce4aa2b0 100644
--- a/packages/form-integrations/test/form-integrations.test.js
+++ b/packages/form-integrations/test/form-integrations.test.js
@@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing';
+import { getAllTagNames } from './helpers/helpers.js';
import './helpers/umbrella-form.js';
/**
@@ -28,6 +29,8 @@ describe('Form Integrations', () => {
lyrics: '1',
range: 2.3,
terms: [],
+ notifications: { value: '', checked: false },
+ rsvp: '',
comments: '',
});
});
@@ -36,6 +39,7 @@ describe('Form Integrations', () => {
const el = /** @type {UmbrellaForm} */ (await fixture(html``));
await el.updateComplete;
const formEl = el._lionFormNode;
+
expect(formEl.formattedValue).to.eql({
full_name: { first_name: '', last_name: '' },
date: '12/12/2000',
@@ -52,6 +56,8 @@ describe('Form Integrations', () => {
lyrics: '1',
range: 2.3,
terms: [],
+ notifications: '',
+ rsvp: '',
comments: '',
});
});
@@ -85,4 +91,54 @@ describe('Form Integrations', () => {
expect(el._lionFormNode.dirty).to.be.false;
});
});
+
+ it('Successfully registers all form components', async () => {
+ const el = /** @type {UmbrellaForm} */ await fixture(html``);
+ // @ts-ignore
+ const formEl = /** @type {LionForm} */ (el._lionFormNode);
+ await formEl.registrationComplete;
+ const registeredEls = getAllTagNames(formEl);
+
+ expect(registeredEls).to.eql([
+ 'lion-fieldset',
+ ' lion-input',
+ ' lion-input',
+ 'lion-input-date',
+ 'lion-input-datepicker',
+ 'lion-textarea',
+ 'lion-input-amount',
+ 'lion-input-iban',
+ 'lion-input-email',
+ 'lion-checkbox-group',
+ ' lion-checkbox',
+ ' lion-checkbox',
+ ' lion-checkbox',
+ 'lion-radio-group',
+ ' lion-radio',
+ ' lion-radio',
+ ' lion-radio',
+ 'lion-listbox',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ 'lion-combobox',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ 'lion-select-rich',
+ ' lion-option',
+ ' lion-option',
+ ' lion-option',
+ 'lion-select',
+ 'lion-input-range',
+ 'lion-checkbox-group',
+ ' lion-checkbox',
+ 'lion-switch',
+ 'lion-input-stepper',
+ 'lion-textarea',
+ ]);
+ });
});
diff --git a/packages/form-integrations/test/form-reset.test.js b/packages/form-integrations/test/form-reset.test.js
deleted file mode 100644
index 59f48b3f7..000000000
--- a/packages/form-integrations/test/form-reset.test.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import '@lion/button/define';
-import '@lion/checkbox-group/define';
-import { MinLength, Required } from '@lion/form-core';
-import '@lion/form/define';
-import '@lion/input-amount/define';
-import '@lion/input-date/define';
-import '@lion/input-datepicker/define';
-import '@lion/input-email/define';
-import '@lion/input-iban/define';
-import '@lion/input-range/define';
-import '@lion/input/define';
-import '@lion/radio-group/define';
-import '@lion/select/define';
-import '@lion/switch/define';
-import '@lion/textarea/define';
-import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
-
-describe(`Submitting/Resetting Form`, async () => {
- /** @type {import('@lion/form').LionForm} */
- let el;
- beforeEach(async () => {
- el = await fixture(html`
-
-
-
- `);
- });
-
- it('Submitting a form should make submitted true for all fields', async () => {
- /** @type {import('@lion/button').LionButton} */ (el.querySelector('#submit_button')).click();
- await elementUpdated(el);
- await el.updateComplete;
- el.formElements.forEach(field => {
- expect(field.submitted).to.be.true;
- });
- });
-
- it('Resetting a form should reset metadata of all fields', async () => {
- /** @type {import('@lion/button').LionButton} */ (el.querySelector('#submit_button')).click();
- /** @type {import('@lion/button').LionButton} */ (el.querySelector('#reset_button')).click();
- await elementUpdated(el);
- await el.updateComplete;
- expect(el.submitted).to.be.false;
- el.formElements.forEach(field => {
- expect(field.submitted).to.be.false;
- expect(field.touched).to.be.false;
- expect(field.dirty).to.be.false;
- });
- });
-});
diff --git a/packages/form-integrations/test/helpers/helpers.js b/packages/form-integrations/test/helpers/helpers.js
new file mode 100644
index 000000000..28999d156
--- /dev/null
+++ b/packages/form-integrations/test/helpers/helpers.js
@@ -0,0 +1,40 @@
+/**
+ * @typedef {import('@lion/form').LionForm} LionForm
+ * @typedef {import('@lion/form-core').LionField} LionField
+ */
+
+/**
+ * @param {LionForm} formGroupEl
+ */
+export function getAllFormElements(formGroupEl) {
+ const getElms = (/** @type {HTMLElement} */ elm) => [
+ elm,
+ // @ts-ignore
+ ...(elm.formElements ? elm.formElements.map(getElms).flat() : []),
+ ];
+
+ // @ts-ignore
+ return formGroupEl.formElements.map(elem => getElms(elem)).flat();
+}
+
+/**
+ * @param {LionForm} formGroupEl
+ */
+export function getAllTagNames(formGroupEl) {
+ const getTagNames = (/** @type {HTMLElement} */ elm, lvl = 0) => [
+ ` `.repeat(lvl) + elm.tagName.toLowerCase(),
+ // @ts-ignore
+ ...(elm.formElements ? elm.formElements.map(elem => getTagNames(elem, lvl + 1)).flat() : []),
+ ];
+
+ // @ts-ignore
+ return formGroupEl.formElements.map(elem => getTagNames(elem)).flat();
+}
+
+/**
+ * @param {LionForm} formGroupEl
+ */
+export function getAllFieldsAndFormGroups(formGroupEl) {
+ const allElements = getAllFormElements(formGroupEl);
+ return allElements.filter((/** @type {LionField} */ elm) => elm.tagName !== 'LION-OPTION');
+}
diff --git a/packages/form-integrations/test/helpers/umbrella-form.js b/packages/form-integrations/test/helpers/umbrella-form.js
index 5efc3c712..eb86d7918 100644
--- a/packages/form-integrations/test/helpers/umbrella-form.js
+++ b/packages/form-integrations/test/helpers/umbrella-form.js
@@ -17,6 +17,8 @@ import '@lion/combobox/define';
import '@lion/input-range/define';
import '@lion/textarea/define';
import '@lion/button/define';
+import '@lion/switch/define';
+import '@lion/input-stepper/define';
export class UmbrellaForm extends LitElement {
get _lionFormNode() {
@@ -131,7 +133,10 @@ export class UmbrellaForm extends LitElement {
name="terms"
.validators="${[new Required()]}"
>
-
+
@@ -140,13 +145,14 @@ export class UmbrellaForm extends LitElement {
- Submit
+ Submit
- // @ts-ignore
- ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()}
+ @click="${() => {
+ this._lionFormNode.resetGroup();
+ }}"
>Reset
diff --git a/packages/form-integrations/test/model-value-consistency.test.js b/packages/form-integrations/test/model-value-consistency.test.js
index 31fbce067..b335a2270 100644
--- a/packages/form-integrations/test/model-value-consistency.test.js
+++ b/packages/form-integrations/test/model-value-consistency.test.js
@@ -10,11 +10,14 @@ import '@lion/input-datepicker/define';
import '@lion/input-email/define';
import '@lion/input-iban/define';
import '@lion/input-range/define';
+import '@lion/input-stepper/define';
+
import '@lion/textarea/define';
import '@lion/checkbox-group/define';
import '@lion/radio-group/define';
+import '@lion/switch/define';
import '@lion/select/define';
@@ -343,11 +346,13 @@ describe('detail.isTriggeredByUser', () => {
'input-email',
'input-iban',
'input-range',
+ 'input-stepper',
'textarea',
// 1b) Choice Fields
'option',
'checkbox',
'radio',
+ 'switch',
// 1c) Choice Group Fields
'select',
'listbox',