Skip to content

Instantly share code, notes, and snippets.

@ker0x
Last active July 26, 2021 21:10
Show Gist options
  • Save ker0x/42af77a3aa2b4ba4eff56537ba762352 to your computer and use it in GitHub Desktop.
Save ker0x/42af77a3aa2b4ba4eff56537ba762352 to your computer and use it in GitHub Desktop.
Enhanced Symfony ChoiceType with Stimulus
<?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;
});
}
}
'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);
}
}
{% 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 %}
<?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;
}
}
<?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;
}
}
'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()
})
}
}
}
}
<?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