Last active
July 26, 2021 21:10
-
-
Save ker0x/42af77a3aa2b4ba4eff56537ba762352 to your computer and use it in GitHub Desktop.
Enhanced Symfony ChoiceType with Stimulus
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Form\Field; | |
use App\Form\Util\StringUtil; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\FormInterface; | |
use Symfony\Component\Form\FormView; | |
use Symfony\Component\OptionsResolver\Options; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
abstract class AbstractEnhancedChoiceType extends AbstractType | |
{ | |
public function buildView(FormView $view, FormInterface $form, array $options): void | |
{ | |
parent::buildView($view, $form, $options); | |
$controllerName = StringUtil::normalizeControllerName($options['controller_name'] ?? $this->getBlockPrefix()); | |
$view->vars['attr']["data-{$controllerName}-target"] = 'input'; | |
// stimulus controller name and values | |
$view->vars['controller_name'] = $controllerName; | |
$view->vars['controller_values']['add-precedence'] = $options['add_precedence']; | |
$view->vars['controller_values']['allow-empty-options'] = $options['allow_empty_option']; | |
$view->vars['controller_values']['close-after-selected'] = $options['close_after_selected']; | |
$view->vars['controller_values']['create'] = $options['create']; | |
$view->vars['controller_values']['create-on-blur'] = $options['create_on_blur']; | |
$view->vars['controller_values']['delimiter'] = $options['delimiter']; | |
$view->vars['controller_values']['diacritics'] = $options['diacritics']; | |
$view->vars['controller_values']['duplicates'] = $options['duplicates']; | |
$view->vars['controller_values']['highlight'] = $options['highlight']; | |
$view->vars['controller_values']['load-throttle'] = $options['load_throttle']; | |
$view->vars['controller_values']['loading-class'] = $options['loading_class']; | |
$view->vars['controller_values']['max-options'] = $options['max_options']; | |
$view->vars['controller_values']['open-on-focus'] = $options['open_on_focus']; | |
$view->vars['controller_values']['persist'] = $options['persist']; | |
$view->vars['controller_values']['preload'] = $options['preload']; | |
$view->vars['controller_values']['select-on-tab'] = $options['select_on_tab']; | |
if (null !== $createFilter = $options['create_filter']) { | |
$view->vars['controller_values']['create-filter'] = $createFilter; | |
} | |
if (null !== $dropdownParent = $options['dropdown_parent']) { | |
$view->vars['controller_values']['dropdown-parent'] = $dropdownParent; | |
} | |
if (null !== $hidePlaceholder = $options['hide_placeholder']) { | |
$view->vars['controller_values']['hide-placeholder'] = $hidePlaceholder; | |
} | |
if (null !== $hideSelected = $options['hide_selected']) { | |
$view->vars['controller_values']['hide-selected'] = $hideSelected; | |
} | |
if (null !== $maxItems = $options['max_items']) { | |
$view->vars['controller_values']['max-items'] = $maxItems; | |
} | |
if (null !== $plugins = $options['plugins']) { | |
$view->vars['controller_values']['plugins'] = $plugins; | |
} | |
if (null !== $searchConjunction = $options['search_conjunction']) { | |
$view->vars['controller_values']['search-conjunction'] = $searchConjunction; | |
} | |
} | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
$resolver->setRequired('controller_name'); | |
$resolver->setDefined([ | |
// general options | |
'add_precedence', | |
'allow_empty_option', | |
'close_after_selected', | |
'create', | |
'create_filter', | |
'create_on_blur', | |
'delimiter', | |
'diacritics', | |
'duplicates', | |
'dropdown_parent', | |
'hide_placeholder', | |
'hide_selected', | |
'highlight', | |
'load_throttle', | |
'loading_class', | |
'max_items', | |
'max_options', | |
'open_on_focus', | |
'persist', | |
'plugins', | |
'preload', | |
'select_on_tab', | |
// searching | |
'copy_classes_to_dropdown', | |
'disabled_field', | |
'label_field', | |
'lock_opt_group_order', | |
'optgroup_field', | |
'optgroup_label_field', | |
'optgroup_value_field', | |
'search_conjunction', | |
'search_field', | |
'sort_field', | |
'value_field', | |
]); | |
$resolver->setAllowedTypes('add_precedence', ['bool']); | |
$resolver->setAllowedTypes('allow_empty_option', ['bool']); | |
$resolver->setAllowedTypes('close_after_selected', ['bool']); | |
$resolver->setAllowedTypes('copy_classes_to_dropdown', ['bool']); | |
$resolver->setAllowedTypes('controller_name', ['string']); | |
$resolver->setAllowedTypes('create', ['bool']); | |
$resolver->setAllowedTypes('create_filter', ['null', 'string']); | |
$resolver->setAllowedTypes('create_on_blur', ['bool']); | |
$resolver->setAllowedTypes('delimiter', ['string']); | |
$resolver->setAllowedTypes('diacritics', ['bool']); | |
$resolver->setAllowedTypes('disabled_field', ['null', 'string']); | |
$resolver->setAllowedTypes('dropdown_parent', ['null', 'string']); | |
$resolver->setAllowedTypes('duplicates', ['bool']); | |
$resolver->setAllowedTypes('hide_placeholder', ['null', 'bool']); | |
$resolver->setAllowedTypes('hide_selected', ['null', 'bool']); | |
$resolver->setAllowedTypes('highlight', ['bool']); | |
$resolver->setAllowedTypes('label_field', ['null', 'string']); | |
$resolver->setAllowedTypes('load_throttle', ['int']); | |
$resolver->setAllowedTypes('loading_class', ['string']); | |
$resolver->setAllowedTypes('lock_opt_group_order', ['bool']); | |
$resolver->setAllowedTypes('max_items', ['null', 'int']); | |
$resolver->setAllowedTypes('max_options', ['int']); | |
$resolver->setAllowedTypes('open_on_focus', ['bool']); | |
$resolver->setAllowedTypes('optgroup_field', ['null', 'string']); | |
$resolver->setAllowedTypes('optgroup_label_field', ['null', 'string']); | |
$resolver->setAllowedTypes('optgroup_value_field', ['null', 'string']); | |
$resolver->setAllowedTypes('persist', ['bool']); | |
$resolver->setAllowedTypes('plugins', ['null', 'array']); | |
$resolver->setAllowedTypes('preload', ['bool', 'string']); | |
$resolver->setAllowedTypes('search_conjunction', ['null', 'string']); | |
$resolver->setAllowedTypes('search_field', ['null', 'array']); | |
$resolver->setAllowedTypes('select_on_tab', ['bool']); | |
$resolver->setAllowedTypes('sort_field', ['null', 'string', 'array']); | |
$resolver->setAllowedTypes('value_field', ['null', 'string']); | |
$resolver->setAllowedValues('preload', [ | |
false, | |
'focus', | |
]); | |
$resolver->setAllowedValues('search_conjunction', [ | |
null, | |
'and', | |
'or', | |
]); | |
$resolver->setDefaults([ | |
'add_precedence' => false, | |
'allow_empty_option' => false, | |
'close_after_selected' => false, | |
'create' => false, | |
'create_filter' => null, | |
'create_on_blur' => false, | |
'delimiter' => ', ', | |
'diacritics' => true, | |
'dropdown_parent' => null, | |
'duplicates' => false, | |
'hide_placeholder' => null, | |
'hide_selected' => null, | |
'highlight' => true, | |
'label_field' => null, | |
'load_throttle' => 300, | |
'loading_class' => 'loading', | |
'lock_opt_group_order' => false, | |
'max_items' => null, | |
'max_options' => 50, | |
'open_on_focus' => true, | |
'persist' => true, | |
'plugins' => null, | |
'preload' => false, | |
'search_conjunction' => null, | |
'search_field' => null, | |
'select_on_tab' => false, | |
'sort_field' => null, | |
'value_field' => null, | |
]); | |
$resolver->setNormalizer('plugins', static function (Options $options, ?array $states): ?string { | |
if (\is_array($states)) { | |
foreach ($states as $key => $value) { | |
if (\is_array($value)) { | |
continue; | |
} | |
$states[$value] = true; | |
unset($states[$key]); | |
} | |
return json_encode($states, JSON_THROW_ON_ERROR); | |
} | |
return null; | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
import { Controller } from 'stimulus' | |
import TomSelect from 'tom-select' | |
export default class extends Controller { | |
static targets = [ | |
'input' | |
] | |
static values = { | |
addPrecedence: Boolean, | |
allowEmptyOption: Boolean, | |
closeAfterSelect: Boolean, | |
copyClassesToDropdown: Boolean, | |
create: Boolean, | |
createFilter: String, | |
createOnBlur: Boolean, | |
delimiter: String, | |
diacritics: Boolean, | |
dropdownParent: String, | |
duplicates: Boolean, | |
hidePlaceholder: Boolean, | |
hideSelected: Boolean, | |
highlight: Boolean, | |
loadThrottle: Number, | |
loadingClass: String, | |
lockOptgroupOrder: Boolean, | |
maxItems: Number, | |
maxOptions: Number, | |
openOnFocus: Boolean, | |
persist: Boolean, | |
plugins: Object, | |
preload: Boolean, | |
selectOnTab: Boolean, | |
searchConjunction: String, | |
searchField: Array, | |
} | |
connect() { | |
this.advancedChoice = new TomSelect(this.inputTarget, { | |
...this.defaultOptions, | |
...this.options | |
}) | |
this._dispatchEvent('advanced-choice:connect', { | |
advancedChoice: this.advancedChoice, | |
defaultOptions: this.defaultOptions, | |
options: this.options, | |
}); | |
} | |
disconnect() { | |
this.advancedChoice.destroy() | |
this.advancedChoice = undefined | |
this._dispatchEvent('advanced-choice:disconnect') | |
} | |
get defaultOptions() { | |
let defaultOptions = { | |
addPrecedence: this.addPrecedenceValue, | |
allowEmptyOption: this.allowEmptyOptionValue, | |
create: this.createValue, | |
createOnBlur: this.createOnBlurValue, | |
delimiter: this.delimiterValue, | |
diacritics: this.diacriticsValue, | |
duplicates: this.duplicatesValue, | |
highlight: this.highlightValue, | |
loadThrottle: this.loadThrottleValue, | |
loadingClass: this.loadingClassValue, | |
maxOptions: this.maxOptionsValue, | |
openOnFocus: this.openOnFocusValue, | |
persist: this.persistValue, | |
preload: this.preloadValue, | |
selectOnTab: this.selectOnTabValue, | |
} | |
if (this.hasCreateFilterValue) { | |
defaultOptions.createFilter = this.createFilterValue | |
} | |
if (this.hasDropdownParentValue) { | |
defaultOptions.dropdownParent = this.dropdownParentValue | |
} | |
if (this.hasHidePlaceholderValue) { | |
defaultOptions.hidePlaceholder = this.hidePlaceholderValue | |
} | |
if (this.hasHideSelectedValue) { | |
defaultOptions.hideSelected = this.hideSelectedValue | |
} | |
if (this.hasMaxItemsValue) { | |
defaultOptions.maxItems = this.maxItemsValue | |
} | |
if (this.hasPluginsValue) { | |
defaultOptions.plugins = this.pluginsValue | |
} | |
return defaultOptions | |
} | |
get options() { | |
return {} | |
} | |
_dispatchEvent(name, payload = null, canBubble = false, cancelable = false) { | |
const userEvent = document.createEvent('CustomEvent'); | |
userEvent.initCustomEvent(name, canBubble, cancelable, payload); | |
this.element.dispatchEvent(userEvent); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% block advanced_choice_widget -%} | |
<div {{ stimulus_controller(controller_name, controller_values) }} class="advanced-choice-container"> | |
{{- form_widget(form) -}} | |
</div> | |
{%- endblock %} | |
{% block advanced_entity_widget -%} | |
{{- block('advanced_choice_widget') -}} | |
{%- endblock %} | |
{% block advanced_language_widget -%} | |
{{- block('advanced_choice_widget') -}} | |
{%- endblock %} | |
{% block searchable_entity_widget -%} | |
{{- block('advanced_choice_widget') -}} | |
{%- endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Form\Field; | |
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |
final class EnhancedChoiceType extends AbstractEnhancedChoiceType | |
{ | |
public function getParent(): string | |
{ | |
return ChoiceType::class; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Form\Field; | |
use Symfony\Bridge\Doctrine\Form\Type\EntityType; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
final class EnhancedEntityType extends AbstractEnhancedChoiceType | |
{ | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
parent::configureOptions($resolver); | |
$resolver->setDefaults([ | |
'controller_name' => 'advanced_choice', | |
]); | |
} | |
public function getParent(): string | |
{ | |
return EntityType::class; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
import AdvancedChoiceController from './advanced-choice_controller'; | |
export default class extends AdvancedChoiceController { | |
static values = { | |
endpoint: String, | |
valueField: String, | |
labelField: String, | |
searchField: Array, | |
} | |
get options() { | |
console.log(this.persistValue) | |
return { | |
valueField: this.valueFieldValue, | |
labelField: this.labelFieldValue, | |
searchField: this.searchFieldValue, | |
load: async (query, callback) => { | |
const url = `${this.endpointValue}?search=${encodeURIComponent(query)}` | |
await fetch(url, { | |
headers: { | |
Accept: 'application/json' | |
} | |
}).then(response => response.json()) | |
.then(json => { | |
callback(json) | |
}) | |
.catch(() => { | |
callback() | |
}) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Form\Field; | |
use Symfony\Bridge\Doctrine\Form\Type\EntityType; | |
use Symfony\Component\Form\FormInterface; | |
use Symfony\Component\Form\FormView; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
final class SearchableEntityType extends AbstractEnhancedChoiceType | |
{ | |
public function buildView(FormView $view, FormInterface $form, array $options): void | |
{ | |
parent::buildView($view, $form, $options); | |
$view->vars['controller_values']['endpoint'] = $options['endpoint']; | |
$view->vars['controller_values']['value-field'] = $options['value_field']; | |
$view->vars['controller_values']['label-field'] = $options['label_field']; | |
$view->vars['controller_values']['search-field'] = $options['search_field']; | |
} | |
public function configureOptions(OptionsResolver $resolver): void | |
{ | |
parent::configureOptions($resolver); | |
$resolver->setRequired('endpoint'); | |
$resolver->setRequired('value_field'); | |
$resolver->setRequired('label_field'); | |
$resolver->setRequired('search_field'); | |
$resolver->setAllowedTypes('endpoint', ['string']); | |
$resolver->setDefaults([ | |
'controller_name' => 'searchable_entity', | |
]); | |
} | |
public function getParent(): string | |
{ | |
return EntityType::class; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment