My book recommendations

How to use Bootstrap buttons with dropdowns in Symfony

symfony_black_03Since Symfony 2.6 you have the possibility to use a Bootstrap (version 3) theme for your forms. But unfortunately this theme is not able to render an input field group with a dropdown button.

bootstrap_dropdownbutton

In this post I will show you how you can achive this, with only a few small adjustments.

Basics

To apply the Bootstrap theme – which is already delivered by Symfony – to all the forms of your application, add the following configuration to app/config/config.yml

twig:
    form_themes:
        - "bootstrap_3_layout.html.twig"

If you want to apply this only for some forms, add the following instruction at the top of the Twig template where the form is defined:

{% form_theme form 'bootstrap_3_layout.html.twig' %

Now your forms are rendered with the Bootstrap theme structure and if you have already included the Bootstrap CSS file, you should already see some awesome forms in your frontend.

Requirements

Thats really easy, the only requirement is that you are working with the Symfony FormBuilder like the following:

# ...

$form = $this->createFormBuilder($search)
    ->add('type', ChoiceType::class, [
        'choices' => [
            'Username' => 'username',
            'Firstname' => 'firstname',
            'Lastname' => 'lastname'
        ]
    ])
    ->add('keyword', TextType::class, [
        'label' => '',
        'attr' => [
            'class' => 'form-control',
            'placeholder' => 'Keyword'
        ]
    ])
    ->add('search', ButtonType::class, [
        'label' => 'Search',
        'attr' => [
            'class' => 'btn btn-default submit'
        ]
    ])
    ->getForm();

# ...

So no rocket sience, only something you already know.

Adjustments

Hurray, now it`s time to create our own Bootstrap layout file so that we are able to add our own stuff.

For this example, create a new file: src/YourBundleName/Resources/views/Form/bootstrap_3_layout.html.twig – but hey, you can store this also below app/Resources/views/…

The next step is to tell Symfony to use this new file instead of the original. Do you rember? Sure … we can do this globally in app/config/config.yml

twig:
    form_themes:
        - "BundleName:Form:bootstrap_3_layout.html.twig"

or just for some forms with:

{% form_theme form 'BundleName:Form:bootstrap_3_layout.html.twig' %}

To avoid duplicate code we could use the template inheritance tag “use”, therefore add the following instruction to our new layout template:

{% use "bootstrap_3_layout.html.twig" %}

At this point Symfony will use our new own Bootstrap theme which is currently empty but inherits the original Bootstrap theme. Or in other words, you shouldn`t see any differences in the frontend now until we don`t overwrite anything.

Without our following adjustments the example form looks… ok! It`s not ugly, it`s ok! But we don`t want a form which is only ok! Correct?

bootstrap_form

What we want is something like that:

bootstrap_dropdownbutton

To achive that, add the following code to our new template, but pay attention, add this below the use tag!

{%- block form_widget_compound -%}
    <div class="input-group input-group-lg">
        {%- if form.parent is empty -%}
            {{ form_errors(form) }}
        {%- endif -%}
        {{- block('form_rows') -}}
        {{- form_rest(form) -}}
    </div>
{%- endblock form_widget_compound -%}

{% block form_row -%}
    {{- form_widget(form) -}}
    {{- form_errors(form) -}}
{%- endblock form_row %}

{% block button_row -%}
    <span class="input-group-btn">
        {{- form_widget(form) -}}
    </span>
{%- endblock button_row %}

{# Type choice #}

{%- block choice_widget_collapsed -%}
    {%- if required and placeholder is none and not placeholder_in_choices and not multiple and (attr.size is not defined or attr.size <= 1) -%}
        {% set required = false %}
    {%- endif -%}

    <div class="input-group-btn">
        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="dropdown-title">Action</span> <span class="caret"></span></button>
        <ul class="dropdown-menu">
            {%- if placeholder is not none -%}
                <li class="dropdown-header">{{ placeholder != '' ? (translation_domain is same as(false) ? placeholder : placeholder|trans({}, translation_domain)) }}</li>
            {%- endif -%}
            {%- if preferred_choices|length > 0 -%}
                {% set options = preferred_choices %}
                {{- block('choice_widget_options') -}}
                {%- if choices|length > 0 and separator is not none -%}
                    <li class="disabled">{{ separator }}</li>
                {%- endif -%}
            {%- endif -%}
            {%- set options = choices -%}
            {{- block('choice_widget_options') -}}
        </ul>
        <input type="hidden" value="" id="{{ id }}" name="{{ full_name }}" class="dropdown-menu_value">
    </div>
{%- endblock choice_widget_collapsed -%}

{%- block choice_widget_options -%}
    {% for group_label, choice in options %}
        {%- if choice is iterable -%}
            <li class="dropdown-header">{{ choice_translation_domain is same as(false) ? group_label : group_label|trans({}, choice_translation_domain) }}</li>
            {% set options = choice %}
            {{- block('choice_widget_options') -}}
        {%- else -%}
            {% set attr = choice.attr %}
            <li {% if choice is selectedchoice(value) %} class="selected"{% endif %}><a href="#" data-value="{{ choice.value }}">{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</a></li>
        {%- endif -%}
    {% endfor %}
{%- endblock choice_widget_options -%}

Now your form should “hopefully” look like the following result:

bootstrap_dropdownbutton

Awesome!!!

But hey, nothing happens if I click the search button! Thats true, its only a button without any “submit” context. We could solve this with a small Javascript snippet. I don`t know if this is a best practice, but it`s working for me. If you have a better solution, please let me know. Here is the Javascript snippet, written in jQuery:

$("form button[class~='submit']").click(function() {
    $(this.form).submit();
});

But thats not all, maybe you have already noticed that the selected value is missing after submitting the form. We have to solve this also by using some Javascript. Add the following code also to your Javascript code:

$('.dropdown-menu li').click(function() {
    var value = $(this).find('a').attr('data-value');
    var label = $(this).find('a').html();

    $(this).parent().parent().find('.dropdown-menu_value').val(value);
    $(this).parent().parent().find('.dropdown-title').html(label);
});

This click event will add the selected value to a hidden input field. Now you should be able to grab also the selected value after a submit.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn