Skip to content

Commit c8a50ad

Browse files
feature #747 [Live] Add proper support for boolean checkboxes + fix bug for non-model checkboxes (weaverryan)
This PR was merged into the 2.x branch. Discussion ---------- [Live] Add proper support for boolean checkboxes + fix bug for non-model checkboxes …del checkboxes | Q | A | ------------- | --- | Bug fix? | yes | New feature? | yes | Tickets | Fix #704 | License | MIT Adds support for checkboxes without a `value` attribute, which will result in a boolean value: `<input type="checkbox" data-model"isEnabled">` This is more of a bug fix, as we documented this use, but didn't properly support it. Also fixes a very small bug that caused #704. Commits ------- a802d36a [Live] Add proper support for boolean checkboxes + fix bug for non-model checkboxes
2 parents 3162f9f + e630852 commit c8a50ad

File tree

8 files changed

+140
-21
lines changed

8 files changed

+140
-21
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ public User $user;
1919
the DOM inside a live component, those changes will now be _kept_ when
2020
the component is re-rendered. This has limitations - see the documentation.
2121

22+
- Boolean checkboxes are now supported. Of a checkbox does **not** have a
23+
`value` attribute, then the associated `LiveProp` will be set to a boolean
24+
when the input is checked/unchecked.
25+
2226
- Added support for setting `writable` to a property that is an object
2327
(previously, only scalar values were supported). The object is passed
2428
through the serializer.

src/LiveComponent/assets/dist/dom_utils.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ValueStore from './Component/ValueStore';
22
import { Directive } from './Directive/directives_parser';
33
import Component from './Component';
4-
export declare function getValueFromElement(element: HTMLElement, valueStore: ValueStore): string | string[] | null;
4+
export declare function getValueFromElement(element: HTMLElement, valueStore: ValueStore): string | string[] | null | boolean;
55
export declare function setValueOnElement(element: HTMLElement, value: any): void;
66
export declare function getAllModelDirectiveFromElements(element: HTMLElement): Directive[];
77
export declare function getModelDirectiveFromElement(element: HTMLElement, throwOnMissing?: boolean): null | Directive;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,17 @@ function normalizeModelName(model) {
156156
function getValueFromElement(element, valueStore) {
157157
if (element instanceof HTMLInputElement) {
158158
if (element.type === 'checkbox') {
159-
const modelNameData = getModelDirectiveFromElement(element);
160-
if (modelNameData === null) {
161-
return null;
159+
const modelNameData = getModelDirectiveFromElement(element, false);
160+
if (modelNameData !== null) {
161+
const modelValue = valueStore.get(modelNameData.action);
162+
if (Array.isArray(modelValue)) {
163+
return getMultipleCheckboxValue(element, modelValue);
164+
}
162165
}
163-
const modelValue = valueStore.get(modelNameData.action);
164-
if (Array.isArray(modelValue)) {
165-
return getMultipleCheckboxValue(element, modelValue);
166+
if (element.hasAttribute('value')) {
167+
return element.checked ? element.getAttribute('value') : null;
166168
}
167-
return element.checked ? inputValue(element) : null;
169+
return element.checked;
168170
}
169171
return inputValue(element);
170172
}
@@ -205,7 +207,12 @@ function setValueOnElement(element, value) {
205207
element.checked = valueFound;
206208
}
207209
else {
208-
element.checked = element.value == value;
210+
if (element.hasAttribute('value')) {
211+
element.checked = element.value == value;
212+
}
213+
else {
214+
element.checked = value;
215+
}
209216
}
210217
return;
211218
}

src/LiveComponent/assets/src/dom_utils.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,25 @@ import Component from './Component';
1111
* elements. In those cases, it will return the "full", final value
1212
* for the model, which includes previously-selected values.
1313
*/
14-
export function getValueFromElement(element: HTMLElement, valueStore: ValueStore): string | string[] | null {
14+
export function getValueFromElement(element: HTMLElement, valueStore: ValueStore): string | string[] | null | boolean {
1515
if (element instanceof HTMLInputElement) {
1616
if (element.type === 'checkbox') {
17-
const modelNameData = getModelDirectiveFromElement(element);
18-
if (modelNameData === null) {
19-
return null;
17+
const modelNameData = getModelDirectiveFromElement(element, false);
18+
if (modelNameData !== null) {
19+
// if there's a model - try to determine if it's an array
20+
const modelValue = valueStore.get(modelNameData.action);
21+
if (Array.isArray(modelValue)) {
22+
return getMultipleCheckboxValue(element, modelValue);
23+
}
2024
}
2125

22-
const modelValue = valueStore.get(modelNameData.action);
23-
if (Array.isArray(modelValue)) {
24-
return getMultipleCheckboxValue(element, modelValue);
26+
// read the attribute directly to avoid the default "on" value
27+
if (element.hasAttribute('value')) {
28+
// if the checkbox has a value="", then the unchecked value is null
29+
return element.checked ? element.getAttribute('value') : null;
2530
}
2631

27-
return element.checked ? inputValue(element) : null;
32+
return element.checked;
2833
}
2934

3035
return inputValue(element);
@@ -86,7 +91,13 @@ export function setValueOnElement(element: HTMLElement, value: any): void {
8691

8792
element.checked = valueFound;
8893
} else {
89-
element.checked = element.value == value;
94+
if (element.hasAttribute('value')) {
95+
// if the checkbox has a value="", then check if it matches
96+
element.checked = element.value == value;
97+
} else {
98+
// no value, treat it like a boolean
99+
element.checked = value;
100+
}
90101
}
91102

92103
return;

src/LiveComponent/assets/test/dom_utils.test.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const createStore = function(props: any = {}): ValueStore {
1717
}
1818

1919
describe('getValueFromElement', () => {
20-
it('Correctly adds data from checked checkbox', () => {
20+
it('Correctly adds data from valued checked checkbox', () => {
2121
const input = document.createElement('input');
2222
input.type = 'checkbox';
2323
input.checked = true;
@@ -34,7 +34,7 @@ describe('getValueFromElement', () => {
3434
.toEqual(['bar', 'the_checkbox_value']);
3535
});
3636

37-
it('Correctly removes data from unchecked checkbox', () => {
37+
it('Correctly removes data from valued unchecked checkbox', () => {
3838
const input = document.createElement('input');
3939
input.type = 'checkbox';
4040
input.checked = false;
@@ -50,7 +50,36 @@ describe('getValueFromElement', () => {
5050
.toEqual(['bar']);
5151
expect(getValueFromElement(input, createStore({ foo: ['bar', 'the_checkbox_value'] })))
5252
.toEqual(['bar']);
53-
})
53+
});
54+
55+
it('Correctly handles boolean checkbox', () => {
56+
const input = document.createElement('input');
57+
input.type = 'checkbox';
58+
input.checked = true;
59+
input.dataset.model = 'foo';
60+
61+
expect(getValueFromElement(input, createStore()))
62+
.toEqual(true);
63+
64+
input.checked = false;
65+
66+
expect(getValueFromElement(input, createStore()))
67+
.toEqual(false);
68+
});
69+
70+
it('Correctly returns for non-model checkboxes', () => {
71+
const input = document.createElement('input');
72+
input.type = 'checkbox';
73+
input.checked = true;
74+
input.value = 'the_checkbox_value';
75+
76+
expect(getValueFromElement(input, createStore()))
77+
.toEqual('the_checkbox_value');
78+
79+
input.checked = false;
80+
expect(getValueFromElement(input, createStore()))
81+
.toEqual(null);
82+
});
5483

5584
it('Correctly sets data from select multiple', () => {
5685
const select = document.createElement('select');
@@ -132,6 +161,24 @@ describe('setValueOnElement', () => {
132161
expect(input.checked).toBeFalsy();
133162
});
134163

164+
it('Checks checkbox with boolean value', () => {
165+
const input = document.createElement('input');
166+
input.type = 'checkbox';
167+
input.checked = false;
168+
169+
setValueOnElement(input, true);
170+
expect(input.checked).toBeTruthy();
171+
});
172+
173+
it('Unchecks checkbox with boolean value', () => {
174+
const input = document.createElement('input');
175+
input.type = 'checkbox';
176+
input.checked = true;
177+
178+
setValueOnElement(input, false);
179+
expect(input.checked).toBeFalsy();
180+
});
181+
135182
it('Sets data onto select multiple', () => {
136183
const select = document.createElement('select');
137184
select.multiple = true;

src/LiveComponent/doc/index.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,53 @@ changed, added or removed::
487487
Writable path values are dehydrated/hydrated using the same process as the top-level
488488
properties (i.e. Symfony's serializer).
489489

490+
Checkboxes, Select Elements Radios & Arrays
491+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
492+
493+
.. versionadded:: 2.8
494+
495+
The ability to use checkboxes to set boolean values was added in LiveComponent 2.8.
496+
497+
Checkboxes can be used to set a boolean or an array of strings::
498+
499+
#[LiveProp(writable: true)]
500+
public bool $agreeToTerms = false;
501+
502+
#[LiveProp(writable: true)]
503+
public array $foods = ['pizza', 'tacos'];
504+
505+
In the template, setting a ``value`` attribute on the checkbox will set that
506+
value on checked. If no ``value`` is set, the checkbox will set a boolean value:
507+
508+
.. code-block:: twig
509+
510+
<input type="checkbox" data-model="agreeToTerms">
511+
512+
<input type="checkbox" data-model="foods" value="pizza">
513+
<input type="checkbox" data-model="foods" value="tacos">
514+
<input type="checkbox" data-model="foods" value="sushi">
515+
516+
``select`` and ``radio`` elements are a bit easier: use these to either set a
517+
single value or an array of values::
518+
519+
#[LiveProp(writable: true)]
520+
public string $meal = 'lunch';
521+
522+
#[LiveProp(writable: true)]
523+
public array $foods = ['pizza', 'tacos'];
524+
525+
.. code-block:: twig
526+
527+
<input type="radio" data-model="meal" value="breakfast">
528+
<input type="radio" data-model="meal" value="lunch">
529+
<input type="radio" data-model="meal" value="dinner">
530+
531+
<select data-model="foods" multiple>
532+
<option value="pizza">Pizza</option>
533+
<option value="tacos">Tacos</option>
534+
<option value="sushi">Sushi</option>
535+
</select>
536+
490537
Allowing an Entity to be Changed to Another
491538
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
492539

ux.symfony.com/assets/bootstrap.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ export const app = startStimulusApp(require.context(
1111
app.debug = process.env.NODE_ENV === 'development';
1212

1313
app.register('clipboard', Clipboard);
14+
app.register('live', Live);
1415
// register any custom, 3rd party controllers here
1516

ux.symfony.com/templates/components/search_packages.html.twig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
class="form-control"
77
>
88

9+
<input type="checkbox" data-payload="5" value="hi">
10+
911
{% if computed.packages|length > 0 %}
1012
<div data-loading="addClass(opacity-50)" class="mt-3 row">
1113
{% for package in computed.packages %}

0 commit comments

Comments
 (0)