diff --git a/.changeset/rich-pens-repeat.md b/.changeset/rich-pens-repeat.md new file mode 100644 index 000000000..14ed7131b --- /dev/null +++ b/.changeset/rich-pens-repeat.md @@ -0,0 +1,5 @@ +--- +'@lion/form-core': patch +--- + +Sync name to parent form group conditionally and allow overriding. Also fix sync properly to prevent infinite loop. diff --git a/packages/form-core/src/choice-group/ChoiceInputMixin.js b/packages/form-core/src/choice-group/ChoiceInputMixin.js index a3709c75e..6fa85485f 100644 --- a/packages/form-core/src/choice-group/ChoiceInputMixin.js +++ b/packages/form-core/src/choice-group/ChoiceInputMixin.js @@ -119,8 +119,7 @@ const ChoiceInputMixinImplementation = superclass => // @ts-expect-error this._parentFormGroup.name !== this.name ) { - // @ts-expect-error not all choice inputs have a name prop, because this mixin does not have a strict contract with form control mixin - this.name = changedProperties.get('name'); + this._syncNameToParentFormGroup(); } } @@ -232,6 +231,20 @@ const ChoiceInputMixinImplementation = superclass => this.__isHandlingUserInput = false; } + /** + * Override this in case of extending ChoiceInputMixin and requiring + * to sync differently with parent form group name + * Right now it checks tag name match where the parent form group tagname + * should include the child field tagname ('checkbox' is included in 'checkbox-group') + */ + _syncNameToParentFormGroup() { + // @ts-expect-error not all choice inputs have a name prop, because this mixin does not have a strict contract with form control mixin + if (this._parentFormGroup.tagName.includes(this.tagName)) { + // @ts-expect-error + this.name = this._parentFormGroup.name; + } + } + /** * @param {boolean} checked */ 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 fac70a9b2..7b37bfcf1 100644 --- a/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js +++ b/packages/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js @@ -6,28 +6,42 @@ import { expect, html, fixture, unsafeStatic } from '@open-wc/testing'; import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js'; import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js'; +class ChoiceInputFoo extends ChoiceInputMixin(LionInput) {} +customElements.define('choice-input-foo', ChoiceInputFoo); +class ChoiceInputBar extends ChoiceInputMixin(LionInput) { + _syncNameToParentFormGroup() { + // Always sync, without conditions + // @ts-expect-error + this.name = this._parentFormGroup.name; + } +} +customElements.define('choice-input-bar', ChoiceInputBar); class ChoiceInput extends ChoiceInputMixin(LionInput) {} -customElements.define('choice-group-input', ChoiceInput); -class ChoiceGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {} -customElements.define('choice-group', ChoiceGroup); +customElements.define('choice-input', ChoiceInput); +class ChoiceInputGroup extends ChoiceGroupMixin(FormGroupMixin(LitElement)) {} +customElements.define('choice-input-group', ChoiceInputGroup); /** * @param {{ parentTagString?:string, childTagString?: string, choiceType?: string}} [config] */ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choiceType } = {}) { const cfg = { - parentTagString: parentTagString || 'choice-group', - childTagString: childTagString || 'choice-group-input', + parentTagString: parentTagString || 'choice-input-group', + childTagString: childTagString || 'choice-input', + childTagStringFoo: 'choice-input-foo', + childTagStringBar: 'choice-input-bar', choiceType: choiceType || 'single', }; const parentTag = unsafeStatic(cfg.parentTagString); const childTag = unsafeStatic(cfg.childTagString); + const childTagFoo = unsafeStatic(cfg.childTagStringFoo); + const childTagBar = unsafeStatic(cfg.childTagStringBar); describe(`ChoiceGroupMixin: ${cfg.parentTagString}`, () => { if (cfg.choiceType === 'single') { it('has a single modelValue representing the currently checked radio value', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'} checked> @@ -42,7 +56,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('has a single formattedValue representing the currently checked radio value', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'} checked> @@ -58,14 +72,14 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi } it('throws if a child element without a modelValue like { value: "foo", checked: false } tries to register', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'} checked> <${childTag} .choiceValue=${'other'}> `)); - const invalidChild = /** @type {ChoiceGroup} */ (await fixture(html` + const invalidChild = /** @type {ChoiceInputGroup} */ (await fixture(html` <${childTag} .modelValue=${'Lara'}> `)); @@ -77,7 +91,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('automatically sets the name property of child fields to its own name', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'female'} checked> <${childTag} .choiceValue=${'other'}> @@ -87,7 +101,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi expect(el.formElements[0].name).to.equal('gender[]'); expect(el.formElements[1].name).to.equal('gender[]'); - const validChild = /** @type {ChoiceGroup} */ (await fixture(html` + const validChild = /** @type {ChoiceInputGroup} */ (await fixture(html` <${childTag} .choiceValue=${'male'}> `)); el.appendChild(validChild); @@ -96,7 +110,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('automatically updates the name property of child fields to its own name', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag}> <${childTag}> @@ -115,7 +129,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('prevents updating the name property of a child if it is different from its parent', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag}> <${childTag}> @@ -131,15 +145,49 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi expect(el.formElements[0].name).to.equal('gender[]'); }); + it('allows updating the name property of a child if parent tagName does not include childTagname', async () => { + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` + <${parentTag} name="gender[]"> + <${childTagFoo}> + <${childTagFoo}> + + `)); + + expect(el.formElements[0].name).to.equal('gender[]'); + expect(el.formElements[1].name).to.equal('gender[]'); + + el.formElements[0].name = 'gender2[]'; + + await el.formElements[0].updateComplete; + expect(el.formElements[0].name).to.equal('gender2[]'); + }); + + it('allows setting the condition for syncing the name property of a child to parent', async () => { + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` + <${parentTag} name="gender[]"> + <${childTagBar}> + <${childTagBar}> + + `)); + + expect(el.formElements[0].name).to.equal('gender[]'); + expect(el.formElements[1].name).to.equal('gender[]'); + + el.formElements[0].name = 'gender2[]'; + + await el.formElements[0].updateComplete; + expect(el.formElements[0].name).to.equal('gender[]'); + }); + it('adjusts the name of a child element if it has a different name than the group', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'female'} checked> <${childTag} .choiceValue=${'other'}> `)); - const invalidChild = /** @type {ChoiceGroup} */ (await fixture(html` + const invalidChild = /** @type {ChoiceInputGroup} */ (await fixture(html` <${childTag} name="foo" .choiceValue=${'male'}> `)); el.addFormElement(invalidChild); @@ -148,7 +196,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('can set initial modelValue on creation', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]" .modelValue=${'other'}> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> @@ -165,7 +213,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('can set initial serializedValue on creation', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]" .serializedValue=${'other'}> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> @@ -182,7 +230,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('can set initial formattedValue on creation', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]" .formattedValue=${'other'}> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> @@ -201,7 +249,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi it('can handle complex data via choiceValue', async () => { const date = new Date(2018, 11, 24, 10, 33, 30, 0); - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="data[]"> <${childTag} .choiceValue=${{ some: 'data' }}> <${childTag} .choiceValue=${date} checked> @@ -220,7 +268,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('can handle 0 and empty string as valid values', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="data[]"> <${childTag} .choiceValue=${0} checked> <${childTag} .choiceValue=${''}> @@ -239,7 +287,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('can check a radio by supplying an available modelValue', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .modelValue="${{ value: 'male', checked: false }}" @@ -264,7 +312,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi it('expect child nodes to only fire one model-value-changed event per instance', async () => { let counter = 0; - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]" @model-value-changed=${() => { @@ -311,7 +359,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('can be required', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]" .validators=${[new Required()]}> <${childTag} .choiceValue=${'male'}> <${childTag} @@ -335,7 +383,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('returns serialized value', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> @@ -350,7 +398,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('returns serialized value on unchecked state', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> @@ -366,7 +414,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi describe('multipleChoice', () => { it('has a single modelValue representing all currently checked values', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} multiple-choice name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'} checked> @@ -382,7 +430,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('has a single serializedValue representing all currently checked values', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} multiple-choice name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'} checked> @@ -398,7 +446,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('has a single formattedValue representing all currently checked values', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} multiple-choice name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'} checked> @@ -414,7 +462,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('can check multiple checkboxes by setting the modelValue', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} multiple-choice name="gender[]"> <${childTag} .choiceValue=${'male'}> <${childTag} .choiceValue=${'female'}> @@ -429,7 +477,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi }); it('unchecks non-matching checkboxes when setting the modelValue', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} multiple-choice name="gender[]"> <${childTag} .choiceValue=${'male'} checked> <${childTag} .choiceValue=${'female'}> @@ -450,7 +498,7 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi describe('Integration with a parent form/fieldset', () => { it('will serialize all children with their serializedValue', async () => { - const el = /** @type {ChoiceGroup} */ (await fixture(html` + const el = /** @type {ChoiceInputGroup} */ (await fixture(html` <${parentTag} name="gender[]"> <${childTag} .choiceValue=${'male'} checked disabled> diff --git a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts index 473cfd206..1862b1c46 100644 --- a/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts +++ b/packages/form-core/types/choice-group/ChoiceInputMixinTypes.d.ts @@ -43,6 +43,8 @@ export declare class ChoiceInputHost { _preventDuplicateLabelClick(ev: Event): void; + _syncNameToParentFormGroup(): void; + _toggleChecked(ev: Event): void; __syncModelCheckedToChecked(checked: boolean): void;