Skip to content

Commit d2fd965

Browse files
IBX-10621: Checkboxes List (#49)
Co-authored-by: mikolaj <[email protected]>
1 parent c942efd commit d2fd965

File tree

16 files changed

+264
-41
lines changed

16 files changed

+264
-41
lines changed

eslint.config.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
import getIbexaConfig from '@ibexa/eslint-config/eslint';
22

3-
export default getIbexaConfig({ react: false });
3+
export default [
4+
...getIbexaConfig({ react: false }),
5+
{
6+
files: ['**/*.ts'],
7+
rules: {
8+
'@typescript-eslint/unbound-method': 'off',
9+
},
10+
},
11+
];
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { BaseInputsList } from '../../partials';
2+
3+
export enum CheckboxesListFieldAction {
4+
Check = 'check',
5+
Uncheck = 'uncheck',
6+
}
7+
8+
export class CheckboxesListField extends BaseInputsList<string[]> {
9+
private _itemsContainer: HTMLDivElement;
10+
11+
static EVENTS = {
12+
...BaseInputsList.EVENTS,
13+
CHANGE: 'ids:checkboxes-list-field:change',
14+
};
15+
16+
constructor(container: HTMLDivElement) {
17+
super(container);
18+
19+
const itemsContainer = container.querySelector<HTMLDivElement>('.ids-choice-inputs-list__items');
20+
21+
if (!itemsContainer) {
22+
throw new Error('CheckboxesListField: Required elements are missing in the container.');
23+
}
24+
25+
this._itemsContainer = itemsContainer;
26+
27+
this.onItemChange = this.onItemChange.bind(this);
28+
}
29+
30+
getItemsCheckboxes() {
31+
const itemsCheckboxes = [
32+
...this._itemsContainer.querySelectorAll<HTMLInputElement>('.ids-choice-input-field .ids-input--checkbox'),
33+
];
34+
35+
return itemsCheckboxes;
36+
}
37+
38+
getValue(): string[] {
39+
const itemsCheckboxes = this.getItemsCheckboxes();
40+
const checkedValues = itemsCheckboxes.reduce((acc: string[], checkbox) => {
41+
if (checkbox.checked) {
42+
acc.push(checkbox.value);
43+
}
44+
45+
return acc;
46+
}, []);
47+
48+
return checkedValues;
49+
}
50+
51+
onItemChange(event: Event) {
52+
if (!(event.target instanceof HTMLInputElement)) {
53+
return;
54+
}
55+
56+
const item = event.target;
57+
const nextValue = this.getValue();
58+
const actionPerformed = item.checked ? CheckboxesListFieldAction.Check : CheckboxesListFieldAction.Uncheck;
59+
60+
this.onChange(nextValue, item.value, actionPerformed);
61+
}
62+
63+
onChange(nextValue: string[], itemValue: string, actionPerformed: CheckboxesListFieldAction) {
64+
const changeEvent = new CustomEvent(CheckboxesListField.EVENTS.CHANGE, {
65+
bubbles: true,
66+
detail: [nextValue, itemValue, actionPerformed],
67+
});
68+
69+
this._container.dispatchEvent(changeEvent);
70+
}
71+
72+
initCheckboxes() {
73+
const itemsCheckboxes = this.getItemsCheckboxes();
74+
75+
itemsCheckboxes.forEach((checkbox) => {
76+
checkbox.addEventListener('change', this.onItemChange, false);
77+
});
78+
}
79+
80+
unbindCheckboxes() {
81+
const itemsCheckboxes = this.getItemsCheckboxes();
82+
83+
itemsCheckboxes.forEach((checkbox) => {
84+
checkbox.removeEventListener('change', this.onItemChange, false);
85+
});
86+
}
87+
88+
reinit() {
89+
super.reinit();
90+
91+
this.unbindCheckboxes();
92+
this.initCheckboxes();
93+
}
94+
95+
init() {
96+
super.init();
97+
98+
this.initCheckboxes();
99+
}
100+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './checkbox_input';
2+
export * from './checkboxes_list_field';

src/bundle/Resources/public/ts/init_components.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { CheckboxInput, CheckboxesListField } from './components/checkbox';
12
import { InputTextField, InputTextInput } from './components/input_text';
23
import { Accordion } from './components/accordion';
34
import { AltRadioInput } from './components/alt_radio/alt_radio_input';
4-
import { CheckboxInput } from './components/checkbox';
55

66
const accordionContainers = document.querySelectorAll<HTMLDivElement>('.ids-accordion:not([data-ids-custom-init])');
77

@@ -27,6 +27,14 @@ checkboxContainers.forEach((checkboxContainer: HTMLDivElement) => {
2727
checkboxInstance.init();
2828
});
2929

30+
const checkboxesFieldContainers = document.querySelectorAll<HTMLDivElement>('.ids-field.ids-field--list:not([data-ids-custom-init])');
31+
32+
checkboxesFieldContainers.forEach((checkboxesFieldContainer: HTMLDivElement) => {
33+
const checkboxesFieldInstance = new CheckboxesListField(checkboxesFieldContainer);
34+
35+
checkboxesFieldInstance.init();
36+
});
37+
3038
const fieldInputTextContainers = document.querySelectorAll<HTMLDivElement>('.ids-field--input-text:not([data-ids-custom-init])');
3139

3240
fieldInputTextContainers.forEach((fieldInputTextContainer: HTMLDivElement) => {

src/bundle/Resources/public/ts/partials/base.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export abstract class Base {
1717
return this._container;
1818
}
1919

20+
reinit() {
21+
// to be overridden in subclasses if needed
22+
}
23+
2024
init() {
2125
this._container.setAttribute('data-ids-initialized', 'true');
2226

src/bundle/Resources/views/themes/standard/design_system/components/checkbox/input.html.twig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
name,
2222
required,
2323
type,
24+
value,
2425
})
2526
}}
2627
/>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% extends '@IbexaDesignSystemTwig/themes/standard/design_system/partials/base_inputs_list.html.twig' %}
2+
3+
{% set class = html_classes('ids-checkboxes-list-field', attributes.render('class') ?? '') %}
4+
5+
{% block item %}
6+
<twig:ibexa:checkbox:field {{ ...item }}>
7+
{{ item.label }}
8+
</twig:ibexa:checkbox:field>
9+
{% endblock item %}

src/lib/Twig/Components/AbstractChoiceInput.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ abstract class AbstractChoiceInput
2929

3030
public string $size = 'medium';
3131

32-
protected ?string $value = null;
32+
public ?string $value = null;
3333

3434
/**
3535
* @param array<string, mixed> $props
@@ -79,12 +79,6 @@ public function validate(array $props): array
7979
return $resolver->resolve($props) + $props;
8080
}
8181

82-
#[ExposeInTemplate('value')]
83-
protected function getValue(): ?string
84-
{
85-
return null;
86-
}
87-
8882
abstract protected function configurePropsResolver(OptionsResolver $resolver): void;
8983

9084
abstract public function getType(): string;

src/lib/Twig/Components/AbstractField.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ abstract class AbstractField
3232

3333
public bool $required = false;
3434

35-
public string $value = '';
36-
3735
/**
3836
* @param array<string, mixed> $props
3937
*
@@ -49,7 +47,6 @@ public function validate(array $props): array
4947
'labelExtra' => [],
5048
'helperTextExtra' => [],
5149
'required' => false,
52-
'value' => '',
5350
]);
5451

5552
$resolver->setRequired(['name']);
@@ -58,7 +55,6 @@ public function validate(array $props): array
5855
$resolver->setAllowedTypes('labelExtra', 'array');
5956
$resolver->setAllowedTypes('helperTextExtra', 'array');
6057
$resolver->setAllowedTypes('required', 'bool');
61-
$resolver->setAllowedTypes('value', 'string');
6258

6359
$resolver->setNormalizer('labelExtra', static function (Options $options, array $attributes) {
6460
return self::assertForbidden($attributes, ['for', 'required'], 'labelExtra');
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\DesignSystemTwig\Twig\Components\Checkbox;
10+
11+
use Ibexa\DesignSystemTwig\Twig\Components\AbstractField;
12+
use Ibexa\DesignSystemTwig\Twig\Components\ListFieldTrait;
13+
use Symfony\Component\OptionsResolver\OptionsResolver;
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @phpstan-type CheckboxItem array{
18+
* value: string|int,
19+
* label: string,
20+
* disabled?: bool
21+
* }
22+
* @phpstan-type CheckboxItems list<CheckboxItem>
23+
*/
24+
#[AsTwigComponent('ibexa:checkbox:list_field')]
25+
final class ListField extends AbstractField
26+
{
27+
use ListFieldTrait;
28+
29+
/** @var array<string|int> */
30+
public array $value = [];
31+
32+
/**
33+
* @param CheckboxItem $item
34+
*
35+
* @return CheckboxItem
36+
*/
37+
protected function modifyListItem(array $item): array
38+
{
39+
$item['checked'] = in_array($item['value'], $this->value, true);
40+
41+
return $item;
42+
}
43+
44+
protected function configurePropsResolver(OptionsResolver $resolver): void
45+
{
46+
$this->validateListFieldProps($resolver);
47+
48+
// TODO: check if items are valid according to Checkbox/Field component
49+
$resolver->setDefaults(['value' => []]);
50+
$resolver->setAllowedTypes('value', 'array');
51+
}
52+
}

0 commit comments

Comments
 (0)