Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@
"typescript": "catalog:"
},
"packageManager": "pnpm@10.10.0"
}
}
12 changes: 10 additions & 2 deletions packages/demo/src/examples/example07.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ <h2 class="bd-title">
<label class="col-sm-2">Single Select </label>

<div class="col-sm-10">
<select name="select1" class="full-width">
<select id="select1" name="select1" class="full-width">
<option value="1">First</option>
<option value="2">Second</option>
<option value="3">Third</option>
Expand All @@ -42,7 +42,7 @@ <h2 class="bd-title">
<label class="col-sm-2">Multiple Select </label>

<div class="col-sm-10">
<select name="select2" class="full-width" data-test="select2" multiple required>
<select id="select2" name="select2" class="full-width" data-test="select2" multiple required>
<option value="1">First</option>
<option value="2">Second</option>
<option value="3">Third</option>
Expand All @@ -51,6 +51,14 @@ <h2 class="bd-title">
</div>
</div>

<div class="mb-3 row">
<label class="col-sm-2">Select with dynamic options</label>

<div class="col-sm-10">
<select id="select3" name="select3" class="full-width" data-test="select3" required></select>
</div>
</div>

<div class="mb-3 row">
<div class="col-sm-10 offset-sm-2">
<button type="button" class="btn btn-primary submit7" data-testid="submit">Submit</button>
Expand Down
12 changes: 11 additions & 1 deletion packages/demo/src/examples/example07.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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 = () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/demo/src/examples/example08.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,24 @@ <h2 class="bd-title">
<select id="group" class="full-width" multiple></select>
</div>
</div>

<div class="mb-3 row">
<label class="col-sm-2">Single Select With Form</label>

<div class="col-sm-10">
<form id="form1">
<select name="select6" id="single-form" class="full-width"></select>
</form>
</div>
</div>

<div class="mb-3 row">
<label class="col-sm-2">Multiple Select With Form</label>

<div class="col-sm-10">
<form id="form2">
<select name="select7" id="multiple-form" class="full-width" multiple></select>
</form>
</div>
</div>
</div>
42 changes: 42 additions & 0 deletions packages/demo/src/examples/example08.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export default class Example {
ms3?: MultipleSelectInstance;
ms4?: MultipleSelectInstance;
ms5?: MultipleSelectInstance;
ms6?: MultipleSelectInstance;
ms7?: MultipleSelectInstance;

mount() {
this.ms1 = multipleSelect('#basic', {
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
}
}
14 changes: 13 additions & 1 deletion packages/multiple-select-vanilla/src/MultipleSelectInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
classNameToList,
convertItemRowToHtml,
createDomElement,
createDomStructureFromData,
emptyElement,
findParent,
getComputedSize,
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rollback this .toString() change, that is what make some tests fail.

});
}

Expand Down
31 changes: 30 additions & 1 deletion packages/multiple-select-vanilla/src/utils/domUtils.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<OptGroupRowData | OptionRowData>, 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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this entire else section could be simplified and replaced with

createDomElement(
  'option',
  {
    value: row.value?.toString() || '',
    disabled: row.disabled || false,
    selected: !!(row.selected || false),
    className: row.classes || '',
    textContent: (row as OptionRowData).text,
  },
  parent,
);

}
});
return parent;
}

/** takes an html block object and converts to a real HTMLElement */
export function convertItemRowToHtml(item: HtmlStruct): HTMLElement {
if (item.hasOwnProperty('tagName')) {
Expand Down
21 changes: 21 additions & 0 deletions playwright/e2e/example07.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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');
});
29 changes: 29 additions & 0 deletions playwright/e2e/example08.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
});
Loading