diff --git a/package.json b/package.json index e82a006e..1a47a0d3 100644 --- a/package.json +++ b/package.json @@ -73,4 +73,4 @@ "typescript": "catalog:" }, "packageManager": "pnpm@10.10.0" -} \ No newline at end of file +} diff --git a/packages/demo/src/examples/example07.html b/packages/demo/src/examples/example07.html index d69bb902..b793ef9a 100644 --- a/packages/demo/src/examples/example07.html +++ b/packages/demo/src/examples/example07.html @@ -29,7 +29,7 @@

- @@ -42,7 +42,7 @@

- @@ -51,6 +51,14 @@

+
+ + +
+ +
+
+
diff --git a/packages/demo/src/examples/example07.ts b/packages/demo/src/examples/example07.ts index 91ffdbe3..0a6921dc 100644 --- a/packages/demo/src/examples/example07.ts +++ b/packages/demo/src/examples/example07.ts @@ -3,9 +3,17 @@ import { type MultipleSelectInstance, multipleSelect } from 'multiple-select-van export default class Example { btnElm?: HTMLButtonElement | null; ms: MultipleSelectInstance[] = []; + ms3?: MultipleSelectInstance; mount() { - this.ms = multipleSelect('select') as MultipleSelectInstance[]; + this.ms = multipleSelect('#select1, #select2') as MultipleSelectInstance[]; + this.ms3 = multipleSelect('#select3', { + lazyData: () => { + return new Promise(resolve => { + resolve({ '1': 'First', '2': 'Second', '3': 'Third', '4': 'Fourth', '5': 'Fifth' }); + }); + }, + }) as MultipleSelectInstance; this.btnElm = document.querySelector('.submit7'); this.btnElm!.addEventListener('click', this.clickListener); } @@ -16,6 +24,8 @@ export default class Example { // destroy ms instance(s) to avoid DOM leaks this.ms.forEach(m => m.destroy()); this.ms = []; + this.ms3?.destroy(); + this.ms3 = undefined; } clickListener = () => { diff --git a/packages/demo/src/examples/example08.html b/packages/demo/src/examples/example08.html index b81ed0ad..5155bb93 100644 --- a/packages/demo/src/examples/example08.html +++ b/packages/demo/src/examples/example08.html @@ -63,4 +63,24 @@

+ +
+ + +
+
+ +
+
+
+ +
+ + +
+
+ +
+
+
diff --git a/packages/demo/src/examples/example08.ts b/packages/demo/src/examples/example08.ts index 4b58f94c..8c2c976d 100644 --- a/packages/demo/src/examples/example08.ts +++ b/packages/demo/src/examples/example08.ts @@ -6,6 +6,8 @@ export default class Example { ms3?: MultipleSelectInstance; ms4?: MultipleSelectInstance; ms5?: MultipleSelectInstance; + ms6?: MultipleSelectInstance; + ms7?: MultipleSelectInstance; mount() { this.ms1 = multipleSelect('#basic', { @@ -157,6 +159,42 @@ export default class Example { }, ], }) as MultipleSelectInstance; + + this.ms6 = multipleSelect('#single-form', { + dataTest: 'select6', + data: [ + { + text: 'June', + value: 6, + }, + { + text: 'July', + value: 7, + }, + { + text: 'August', + value: 8, + }, + ], + }) as MultipleSelectInstance; + + this.ms7 = multipleSelect('#multiple-form', { + dataTest: 'select7', + data: [ + { + text: 'June', + value: 6, + }, + { + text: 'July', + value: 7, + }, + { + text: 'August', + value: 8, + }, + ], + }) as MultipleSelectInstance; } unmount() { @@ -166,10 +204,14 @@ export default class Example { this.ms3?.destroy(); this.ms4?.destroy(); this.ms5?.destroy(); + this.ms6?.destroy(); + this.ms7?.destroy(); this.ms1 = undefined; this.ms2 = undefined; this.ms3 = undefined; this.ms4 = undefined; this.ms5 = undefined; + this.ms6 = undefined; + this.ms7 = undefined; } } diff --git a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts index 43006d53..8303dae8 100644 --- a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts +++ b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts @@ -13,6 +13,7 @@ import { classNameToList, convertItemRowToHtml, createDomElement, + createDomStructureFromData, emptyElement, findParent, getComputedSize, @@ -335,9 +336,19 @@ export class MultipleSelectInstance { this.data = this.data.sort(this.options.preSort); } + if (!this.fromHtml) { + this.initHtmlRows(); + } + this.dataTotal = setDataKeys(this.data || []); } + protected initHtmlRows() { + this.elm.innerHTML = ''; + if (!this.data) return; + return createDomStructureFromData(this.data, this.elm); + } + protected initRow(elm: HTMLOptionElement, groupDisabled?: boolean) { const row = {} as OptionRowData | OptGroupRowData; if (elm.tagName?.toLowerCase() === 'option') { @@ -1246,6 +1257,7 @@ export class MultipleSelectInstance { // when data is ready, remove spinner & update dropdown and selection this.options.data = data; this._isLazyLoaded = true; + this.fromHtml = false; this.dropElm?.querySelector('.ms-loading')?.remove(); this.initData(); this.initList(true); @@ -1553,7 +1565,7 @@ export class MultipleSelectInstance { } else { // when multiple values could be set, we need to loop through each Array.from(this.elm.options).forEach(option => { - option.selected = selectedValues.some(val => val === option.value); + option.selected = selectedValues.some(val => val.toString() === option.value); }); } diff --git a/packages/multiple-select-vanilla/src/utils/domUtils.ts b/packages/multiple-select-vanilla/src/utils/domUtils.ts index 7a0e6eb3..75102b42 100644 --- a/packages/multiple-select-vanilla/src/utils/domUtils.ts +++ b/packages/multiple-select-vanilla/src/utils/domUtils.ts @@ -1,4 +1,4 @@ -import type { HtmlStruct, InferDOMType } from '../models/interfaces.js'; +import type { HtmlStruct, InferDOMType, OptGroupRowData, OptionRowData } from '../models/interfaces.js'; import { isDefined, objectRemoveEmptyProps } from './utils.js'; export interface HtmlElementPosition { @@ -102,6 +102,35 @@ export function createDomStructure(item: HtmlStruct, appendToElm?: HTMLElement, return elm; } +/** + * Create html node with optgroups and options from data + * @param data - array of options and/or optgroups + * @param parent - parent element to append to + * @return {object} element - updated element + */ +export function createDomStructureFromData(data: Array, parent: HTMLElement): HTMLElement { + data.forEach(row => { + if (row.type === 'optgroup') { + const optgroup = createDomElement('optgroup', { label: (row as OptGroupRowData).label }, parent); + if ((row as OptGroupRowData).children) { + createDomStructureFromData((row as OptGroupRowData).children, optgroup); + } + } else { + const optionProps: any = { + value: row.value, + disabled: row.disabled || false, + selected: row.selected || false, + }; + if (row.classes) { + optionProps.className = row.classes; + } + const option = createDomElement('option', optionProps, parent); + option.textContent = (row as OptionRowData).text; + } + }); + return parent; +} + /** takes an html block object and converts to a real HTMLElement */ export function convertItemRowToHtml(item: HtmlStruct): HTMLElement { if (item.hasOwnProperty('tagName')) { diff --git a/playwright/e2e/example07.spec.ts b/playwright/e2e/example07.spec.ts index 39d23bf4..ce1c66d7 100644 --- a/playwright/e2e/example07.spec.ts +++ b/playwright/e2e/example07.spec.ts @@ -25,6 +25,7 @@ test.describe('Example 07 - Submit Data', () => { }); await page.goto('#/example07'); + await page.locator('[data-test=select2].ms-parent').click(); await page.getByRole('option').filter({ hasText: 'Third' }).locator('span').click(); await page.getByRole('option').filter({ hasText: 'Fourth' }).locator('span').click(); @@ -41,5 +42,25 @@ test.describe('Example 07 - Submit Data', () => { await page.locator('[data-test=select2].ms-parent').click(); await page.getByTestId('submit').click(); await expect(dialogText).toBe('select1=1&select2=1&select2=2'); + }); }); + +test('submit form with multiple select populated via lazy load', async ({ + page +}) => { + let dialogText = ''; + page.on('dialog', async (alert) => { + dialogText = alert.message(); + await alert.dismiss(); + }); + + await page.goto('#/example07'); + await page.waitForTimeout(1); + + // select lazy loaded data + await page.locator('[data-test=select3].ms-parent').click(); + await page.getByRole('option').filter({ hasText: 'First' }).locator('span').click(); + await page.getByTestId('submit').click(); + await expect(dialogText).toBe('select1=1&select3=1'); +}); diff --git a/playwright/e2e/example08.spec.ts b/playwright/e2e/example08.spec.ts index 7eca0e5f..10c1a4da 100644 --- a/playwright/e2e/example08.spec.ts +++ b/playwright/e2e/example08.spec.ts @@ -43,4 +43,33 @@ test.describe('Example 08 - Data Property', () => { await page.getByRole('option', { name: 'Group 1' }).click(); await page.getByRole('button', { name: '11 of 12 selected' }).click(); }); + + test('formdata should be updated after select for regular select', async({ page }) => { + await page.goto('#/example08'); + await page.locator('div[data-test=select6].ms-parent').click(); + await page.getByRole('option', { name: 'July' }).click(); + + const selectedItemValue = await page.evaluate(() => { + const form = document.getElementById('form1'); + const formData = new FormData(form); + return formData.get('select6'); + }); + + expect(selectedItemValue).toBe("7"); + }); + + test('formdata should be updated after select for multiple select', async({ page }) => { + await page.goto('#/example08'); + await page.locator('div[data-test=select7].ms-parent').click(); + await page.getByRole('option', { name: 'July' }).click(); + await page.getByRole('option', { name: 'August' }).click(); + + const selectedItemValue = await page.evaluate(() => { + const form = document.getElementById('form2'); + const formData = new FormData(form); + return formData.getAll('select7'); + }); + + expect(selectedItemValue).toEqual(["7", "8"]); + }); });