Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
10 changes: 9 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import getIbexaConfig from '@ibexa/eslint-config/eslint';

export default getIbexaConfig({ react: false });
export default [
...getIbexaConfig({ react: false }),
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/unbound-method': 'off',
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've switched it off because it returns false-positive on https://github.com/ibexa/design-system-twig/pull/49/files#diff-914f9d057f37b814c2bb03196746b89ce20143f6ece03374618eb28d2472d225R76
Basically linter for TS is not able to recognize if method was bound in constructor (typescript-eslint/typescript-eslint#636), as it's our common way for binding I think switching it off globally is better than leave it globally and switching it off per usecase

},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { BaseInputsList } from '../../partials';

export enum CheckboxesListFieldAction {
Check = 'check',
Uncheck = 'uncheck',
}

export class CheckboxesListField extends BaseInputsList<string[]> {
private _itemsContainer: HTMLDivElement;

static EVENTS = {
...BaseInputsList.EVENTS,
CHANGE: 'ids:checkboxes-list-field:change',
};

constructor(container: HTMLDivElement) {
super(container);

const itemsContainer = container.querySelector<HTMLDivElement>('.ids-choice-inputs-list__items');

if (!itemsContainer) {
throw new Error('CheckboxesListField: Required elements are missing in the container.');
}

this._itemsContainer = itemsContainer;

this.onItemChange = this.onItemChange.bind(this);
}

getItemsCheckboxes() {
const itemsCheckboxes = [
...this._itemsContainer.querySelectorAll<HTMLInputElement>('.ids-choice-input-field .ids-input--checkbox'),
];

return itemsCheckboxes;
}

getValue(): string[] {
const itemsCheckboxes = this.getItemsCheckboxes();
const checkedValues = itemsCheckboxes.reduce((acc: string[], checkbox) => {
if (checkbox.checked) {
acc.push(checkbox.value);
}

return acc;
}, []);

return checkedValues;
}

onItemChange(event: Event) {
if (!(event.target instanceof HTMLInputElement)) {
return;
}

const item = event.target;
const nextValue = this.getValue();
const actionPerformed = item.checked ? CheckboxesListFieldAction.Check : CheckboxesListFieldAction.Uncheck;

this.onChange(nextValue, item.value, actionPerformed);
}

onChange(nextValue: string[], itemValue: string, actionPerformed: CheckboxesListFieldAction) {
const changeEvent = new CustomEvent(CheckboxesListField.EVENTS.CHANGE, {
bubbles: true,
detail: [nextValue, itemValue, actionPerformed],
});

this._container.dispatchEvent(changeEvent);
}

initCheckboxes() {
const itemsCheckboxes = this.getItemsCheckboxes();

itemsCheckboxes.forEach((checkbox) => {
checkbox.addEventListener('change', this.onItemChange, false);
});
}

unbindCheckboxes() {
const itemsCheckboxes = this.getItemsCheckboxes();

itemsCheckboxes.forEach((checkbox) => {
checkbox.removeEventListener('change', this.onItemChange, false);
});
}

reinit() {
super.reinit();

this.unbindCheckboxes();
this.initCheckboxes();
}

init() {
super.init();

this.initCheckboxes();
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './checkbox_input';
export * from './checkboxes_list_field';
10 changes: 9 additions & 1 deletion src/bundle/Resources/public/ts/init_components.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CheckboxInput, CheckboxesListField } from './components/checkbox';
import { InputTextField, InputTextInput } from './components/input_text';
import { Accordion } from './components/accordion';
import { AltRadioInput } from './components/alt_radio/alt_radio_input';
import { CheckboxInput } from './components/checkbox';

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

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

const checkboxesFieldContainers = document.querySelectorAll<HTMLDivElement>('.ids-field.ids-field--list:not([data-ids-custom-init])');

checkboxesFieldContainers.forEach((checkboxesFieldContainer: HTMLDivElement) => {
const checkboxesFieldInstance = new CheckboxesListField(checkboxesFieldContainer);

checkboxesFieldInstance.init();
});

const fieldInputTextContainers = document.querySelectorAll<HTMLDivElement>('.ids-field--input-text:not([data-ids-custom-init])');

fieldInputTextContainers.forEach((fieldInputTextContainer: HTMLDivElement) => {
Expand Down
4 changes: 4 additions & 0 deletions src/bundle/Resources/public/ts/partials/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export abstract class Base {
return this._container;
}

reinit() {
// to be overridden in subclasses if needed
}

init() {
this._container.setAttribute('data-ids-initialized', 'true');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
name,
required,
type,
value,
})
}}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends '@IbexaDesignSystemTwig/themes/standard/design_system/partials/base_inputs_list.html.twig' %}

{% set class = html_classes('ids-checkboxes-list-field', attributes.render('class') ?? '') %}

{% block item %}
<twig:ibexa:checkbox:field {{ ...item }}>
{{ item.label }}
</twig:ibexa:checkbox:field>
{% endblock item %}
8 changes: 1 addition & 7 deletions src/lib/Twig/Components/AbstractChoiceInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ abstract class AbstractChoiceInput

public string $size = 'medium';

protected ?string $value = null;
public ?string $value = null;

/**
* @param array<string, mixed> $props
Expand Down Expand Up @@ -79,12 +79,6 @@ public function validate(array $props): array
return $resolver->resolve($props) + $props;
}

#[ExposeInTemplate('value')]
protected function getValue(): ?string
{
return null;
}

abstract protected function configurePropsResolver(OptionsResolver $resolver): void;

abstract public function getType(): string;
Expand Down
4 changes: 0 additions & 4 deletions src/lib/Twig/Components/AbstractField.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ abstract class AbstractField

public bool $required = false;

public string $value = '';

/**
* @param array<string, mixed> $props
*
Expand All @@ -49,7 +47,6 @@ public function validate(array $props): array
'labelExtra' => [],
'helperTextExtra' => [],
'required' => false,
'value' => '',
]);

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

$resolver->setNormalizer('labelExtra', static function (Options $options, array $attributes) {
return self::assertForbidden($attributes, ['for', 'required'], 'labelExtra');
Expand Down
53 changes: 53 additions & 0 deletions src/lib/Twig/Components/Checkbox/ListField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\DesignSystemTwig\Twig\Components\Checkbox;

use Ibexa\DesignSystemTwig\Twig\Components\AbstractField;
use Ibexa\DesignSystemTwig\Twig\Components\ListFieldTrait;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

/**
* @phpstan-type CheckboxItem array{
* value: string|int,
* label: string,
* disabled?: bool
* }
* @phpstan-type CheckboxItems list<CheckboxItem>
*/
#[AsTwigComponent('ibexa:checkbox:list_field')]
final class ListField extends AbstractField
{
use ListFieldTrait;

/** @var array<string|int> */
public array $value = [];

/**
* @param CheckboxItem $item
*
* @return CheckboxItem
*/
protected function modifyListItem(array $item): array
{
$item['checked'] = in_array($item['value'], $this->value, true);

return $item;
}

protected function configurePropsResolver(OptionsResolver $resolver): void
{
$this->validateListFieldProps($resolver);

// TODO: check if items are valid according to Checkbox/Field component

$resolver->setDefaults(['value' => []]);
$resolver->setAllowedTypes('value', 'array');
}
}
4 changes: 4 additions & 0 deletions src/lib/Twig/Components/InputText/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ final class Field extends AbstractField

public string $type = 'input-text';

public string $value = '';

/**
* @return AttrMap
*/
Expand Down Expand Up @@ -63,5 +65,7 @@ protected function configurePropsResolver(OptionsResolver $resolver): void
});
$resolver->setRequired(['name']);
$resolver->setAllowedTypes('id', ['null', 'string']);
$resolver->setDefaults(['value' => '']);
$resolver->setAllowedTypes('value', 'string');
}
}
4 changes: 1 addition & 3 deletions src/lib/Twig/Components/LabelledChoiceInputTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ public function getInput(): array
'disabled' => $this->disabled,
'error' => $this->error,
'required' => $this->required,
'value' => $this->getValue(),
'value' => $this->value,
];
}

abstract protected function getValue(): ?string;
}
66 changes: 66 additions & 0 deletions src/lib/Twig/Components/ListFieldTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\DesignSystemTwig\Twig\Components;

use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

/**
* @phpstan-type ListItem array{
* value: string|int,
* label: string,
* }
* @phpstan-type ListItems list<ListItem>
*/
trait ListFieldTrait
{
public const string VERTICAL = 'vertical';
public const string HORIZONTAL = 'horizontal';

public string $direction = 'vertical';

/** @var ListItems */
#[ExposeInTemplate(name: 'items', getter: 'getItems')]
public array $items = [];

/**
* @return ListItems
*/
public function getItems(): array
{
return array_map(function (array $item) {
$listItem = $item + ['name' => $this->name, 'required' => $this->required];

return $this->modifyListItem($listItem);
}, $this->items);
}

/**
* @param ListItem $item
*
* @return ListItem
*/
protected function modifyListItem(array $item): array
{
return $item;
}

protected function validateListFieldProps(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'items' => [],
]);
$resolver->setAllowedTypes('items', 'array');

$resolver
->define('direction')
->allowedValues(self::VERTICAL, self::HORIZONTAL)
->default(self::VERTICAL);
}
}
Loading