Skip to content

[LiveComponent] Fix array valued checkboxes change event handling #910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2023
Merged
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
9 changes: 5 additions & 4 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,18 +311,19 @@ function getElementAsTagText(element) {
: element.outerHTML;
}
const getMultipleCheckboxValue = function (element, currentValues) {
const finalValues = [...currentValues];
const value = inputValue(element);
const index = currentValues.indexOf(value);
if (element.checked) {
if (index === -1) {
currentValues.push(value);
finalValues.push(value);
}
return currentValues;
return finalValues;
}
if (index > -1) {
currentValues.splice(index, 1);
finalValues.splice(index, 1);
}
return currentValues;
return finalValues;
};
const inputValue = function (element) {
return element.dataset.value ? element.dataset.value : element.value;
Expand Down
9 changes: 5 additions & 4 deletions src/LiveComponent/assets/src/dom_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,24 +266,25 @@ export function getElementAsTagText(element: HTMLElement): string {
}

const getMultipleCheckboxValue = function (element: HTMLInputElement, currentValues: Array<string>): Array<string> {
const finalValues = [...currentValues];
const value = inputValue(element);
const index = currentValues.indexOf(value);

if (element.checked) {
// Add value to an array if it's not in it already
if (index === -1) {
currentValues.push(value);
finalValues.push(value);
}

return currentValues;
return finalValues;
}

// Remove value from an array
if (index > -1) {
currentValues.splice(index, 1);
finalValues.splice(index, 1);
}

return currentValues;
return finalValues;
};

const inputValue = function (element: HTMLInputElement): string {
Expand Down
62 changes: 62 additions & 0 deletions src/LiveComponent/assets/test/controller/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,38 @@ describe('LiveController data-model Tests', () => {
expect(test.component.valueStore.getOriginalProps()).toEqual({form: {check: ['foo', 'bar']}});
});

it('sends correct data for array valued checkbox fields with non-form object', async () => {
const test = await createTest({ check: [] }, (data: any) => `
<div ${initComponent(data)}>
<form data-model="*">
<label>
Checkbox 1: <input type="checkbox" name="check[]" value="foo" ${data.check.indexOf('foo') > -1 ? 'checked' : ''} />
</label>

<label>
Checkbox 2: <input type="checkbox" name="check[]" value="bar" ${data.check.indexOf('bar') > -1 ? 'checked' : ''} />
</label>
</form>

Checkbox 2 is ${data.check.indexOf('bar') > -1 ? 'checked' : 'unchecked' }
</div>
`);

const check1Element = getByLabelText(test.element, 'Checkbox 1:');
const check2Element = getByLabelText(test.element, 'Checkbox 2:');

// only 1 Ajax call will be made thanks to debouncing
test.expectsAjaxCall()
.expectUpdatedData({ 'check': ['foo', 'bar'] });

await userEvent.click(check1Element);
await userEvent.click(check2Element);

await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 2 is checked'));

expect(test.component.valueStore.getOriginalProps()).toEqual({check: ['foo', 'bar']});
});

it('sends correct data for array valued checkbox fields with initial data', async () => {
const test = await createTest({ form: { check: ['foo']} }, (data: any) => `
<div ${initComponent(data)}>
Expand Down Expand Up @@ -367,6 +399,36 @@ describe('LiveController data-model Tests', () => {
expect(test.component.valueStore.getOriginalProps()).toEqual({form: {check: ['bar']}});
});

it('sends correct data for array valued checkbox fields with non-form object and with initial data', async () => {
const test = await createTest({ check: ['foo'] }, (data: any) => `
<div ${initComponent(data)}>
<label>
Checkbox 1: <input type="checkbox" data-model="check[]" value="foo" ${data.check.indexOf('foo') > -1 ? 'checked' : ''} />
</label>

<label>
Checkbox 2: <input type="checkbox" data-model="check[]" value="bar" ${data.check.indexOf('bar') > -1 ? 'checked' : ''} />
</label>

Checkbox 1 is ${data.check.indexOf('foo') > -1 ? 'checked' : 'unchecked' }
</div>
`);

const check1Element = getByLabelText(test.element, 'Checkbox 1:');
const check2Element = getByLabelText(test.element, 'Checkbox 2:');

// only 1 Ajax call will be made thanks to debouncing
test.expectsAjaxCall()
.expectUpdatedData({ 'check': ['bar'] });

await userEvent.click(check1Element);
await userEvent.click(check2Element);

await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 1 is unchecked'));

expect(test.component.valueStore.getOriginalProps()).toEqual({check: ['bar']});
});

it('sends correct data for select multiple field', async () => {
const test = await createTest({ form: { select: []} }, (data: any) => `
<div ${initComponent(data)}>
Expand Down
6 changes: 3 additions & 3 deletions src/LiveComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,9 @@ value on checked. If no ``value`` is set, the checkbox will set a boolean value:

<input type="checkbox" data-model="agreeToTerms">

<input type="checkbox" data-model="foods" value="pizza">
<input type="checkbox" data-model="foods" value="tacos">
<input type="checkbox" data-model="foods" value="sushi">
<input type="checkbox" data-model="foods[]" value="pizza">
<input type="checkbox" data-model="foods[]" value="tacos">
<input type="checkbox" data-model="foods[]" value="sushi">

``select`` and ``radio`` elements are a bit easier: use these to either set a
single value or an array of values::
Expand Down