Skip to content

Commit 8c35d91

Browse files
committed
feature #21751 Bootstrap4 support for Twig form theme (hiddewie, javiereguiluz)
This PR was merged into the 3.4 branch. Discussion ---------- Bootstrap4 support for Twig form theme **This PR is a followup from #19648. That PR was closed because GitHub thought my branch contained no commits after a force push...** | Q | A | | --- | --- | | Branch? | master | | Bug fix? | no | | New feature? | yes | | BC breaks? | no | | Deprecations? | no | | Tests pass? | yes | | Fixed tickets | #16289 | | License | MIT | | Doc PR | - | I have made a port of the Twig form theming code for Bootstrap 3 to the α-5 version of Bootstrap 4. - The (inheritance) structure of the form theming files has changed because a number of blocks are the same between BS 3 and 4. They have been migrated to `bootstrap_base_layout.html.twig`. The new tree is as follows: ``` bootstrap_base_layout.html.twig |-- bootstrap_3_layout.html.twig | `-- bootstrap_3_horizontal_layout.html.twig `-- bootstrap_4_layout.html.twig `-- bootstrap_4_horizontal_layout.html.twig ``` - Any occurances of `.form-horizontal` have been removed from the BS 4 code. - Checkboxes and radio buttons have been stacked using the `.form-check`, `.form-check-label` and `.form-check-input` classes. There is now no distinction between checkboxes and radio buttons in the markdown. - All layout tests have been added and updated for BS4. The inheritance tree is as follows: ``` AbstractLayoutTest `-- AbstractBootstrap3LayoutTest |-- AbstractBootstrap3HorizontalLayoutTest `-- AbstractBootstrap4LayoutTest `-- AbstractBootstrap4HorizontalLayoutTest ``` All tests pass. The classes `FormExtensionBootstrap4LayoutTest` and `FormExtensionBootstrap4HorizontalLayoutTest` have been created similarly to the BS 3 versions. - ~~The label coloring on an validation is not correct. I've made an issue (twbs/bootstrap#20535) of the problem.~~ - No [custom form elements](http://v4-alpha.getbootstrap.com/components/forms/#custom-forms) have been used. - A docs PR can be created if this PR is accepted. - The new code might have to be updated if large changes occur in the BS 4 α. Screenshot of BS3 and 4 comparison for the same form: ![1](https://cloud.githubusercontent.com/assets/1073881/17732594/dfcb50d6-6472-11e6-8e96-c46987809322.PNG) Commits ------- f12e588 Removed unneeded wrapping quotes around a Twig key 709f134 Removed unneeded wrapping quotes around a Twig key 4222d54 Initial commit for Bootstrap 4 form theme, based on Bootstrap 3 form theme
2 parents 4f89386 + f12e588 commit 8c35d91

12 files changed

+1786
-163
lines changed

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig

-20
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,6 @@ col-sm-2
3434
{##}</div>
3535
{%- endblock form_row %}
3636

37-
{% block checkbox_row -%}
38-
{{- block('checkbox_radio_row') -}}
39-
{%- endblock checkbox_row %}
40-
41-
{% block radio_row -%}
42-
{{- block('checkbox_radio_row') -}}
43-
{%- endblock radio_row %}
44-
45-
{% block checkbox_radio_row -%}
46-
{% spaceless %}
47-
<div class="form-group{% if not valid %} has-error{% endif %}">
48-
<div class="{{ block('form_label_class') }}"></div>
49-
<div class="{{ block('form_group_class') }}">
50-
{{ form_widget(form) }}
51-
{{ form_errors(form) }}
52-
</div>
53-
</div>
54-
{% endspaceless %}
55-
{%- endblock checkbox_radio_row %}
56-
5737
{% block submit_row -%}
5838
{% spaceless %}
5939
<div class="form-group">

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig

+2-138
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% use "form_div_layout.html.twig" %}
1+
{% use "bootstrap_base_layout.html.twig" %}
22

33
{# Widgets #}
44

@@ -9,146 +9,10 @@
99
{{- parent() -}}
1010
{%- endblock form_widget_simple %}
1111

12-
{% block textarea_widget -%}
13-
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %}
14-
{{- parent() -}}
15-
{%- endblock textarea_widget %}
16-
1712
{% block button_widget -%}
1813
{% set attr = attr|merge({class: (attr.class|default('btn-default') ~ ' btn')|trim}) %}
1914
{{- parent() -}}
20-
{%- endblock %}
21-
22-
{% block money_widget -%}
23-
<div class="input-group">
24-
{% set append = money_pattern starts with '{{' %}
25-
{% if not append %}
26-
<span class="input-group-addon">{{ money_pattern|replace({ '{{ widget }}':''}) }}</span>
27-
{% endif %}
28-
{{- block('form_widget_simple') -}}
29-
{% if append %}
30-
<span class="input-group-addon">{{ money_pattern|replace({ '{{ widget }}':''}) }}</span>
31-
{% endif %}
32-
</div>
33-
{%- endblock money_widget %}
34-
35-
{% block percent_widget -%}
36-
<div class="input-group">
37-
{{- block('form_widget_simple') -}}
38-
<span class="input-group-addon">%</span>
39-
</div>
40-
{%- endblock percent_widget %}
41-
42-
{% block datetime_widget -%}
43-
{% if widget == 'single_text' %}
44-
{{- block('form_widget_simple') -}}
45-
{% else -%}
46-
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
47-
<div {{ block('widget_container_attributes') }}>
48-
{{- form_errors(form.date) -}}
49-
{{- form_errors(form.time) -}}
50-
{{- form_widget(form.date, { datetime: true } ) -}}
51-
{{- form_widget(form.time, { datetime: true } ) -}}
52-
</div>
53-
{%- endif %}
54-
{%- endblock datetime_widget %}
55-
56-
{% block date_widget -%}
57-
{% if widget == 'single_text' %}
58-
{{- block('form_widget_simple') -}}
59-
{% else -%}
60-
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
61-
{% if datetime is not defined or not datetime -%}
62-
<div {{ block('widget_container_attributes') -}}>
63-
{%- endif %}
64-
{{- date_pattern|replace({
65-
'{{ year }}': form_widget(form.year),
66-
'{{ month }}': form_widget(form.month),
67-
'{{ day }}': form_widget(form.day),
68-
})|raw -}}
69-
{% if datetime is not defined or not datetime -%}
70-
</div>
71-
{%- endif -%}
72-
{% endif %}
73-
{%- endblock date_widget %}
74-
75-
{% block time_widget -%}
76-
{% if widget == 'single_text' %}
77-
{{- block('form_widget_simple') -}}
78-
{% else -%}
79-
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
80-
{% if datetime is not defined or false == datetime -%}
81-
<div {{ block('widget_container_attributes') -}}>
82-
{%- endif -%}
83-
{{- form_widget(form.hour) }}{% if with_minutes %}:{{ form_widget(form.minute) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second) }}{% endif %}
84-
{% if datetime is not defined or false == datetime -%}
85-
</div>
86-
{%- endif -%}
87-
{% endif %}
88-
{%- endblock time_widget %}
89-
90-
{%- block dateinterval_widget -%}
91-
{%- if widget == 'single_text' -%}
92-
{{- block('form_widget_simple') -}}
93-
{%- else -%}
94-
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
95-
<div {{ block('widget_container_attributes') }}>
96-
{{- form_errors(form) -}}
97-
<div class="table-responsive">
98-
<table class="table {{ table_class|default('table-bordered table-condensed table-striped') }}">
99-
<thead>
100-
<tr>
101-
{%- if with_years %}<th>{{ form_label(form.years) }}</th>{% endif -%}
102-
{%- if with_months %}<th>{{ form_label(form.months) }}</th>{% endif -%}
103-
{%- if with_weeks %}<th>{{ form_label(form.weeks) }}</th>{% endif -%}
104-
{%- if with_days %}<th>{{ form_label(form.days) }}</th>{% endif -%}
105-
{%- if with_hours %}<th>{{ form_label(form.hours) }}</th>{% endif -%}
106-
{%- if with_minutes %}<th>{{ form_label(form.minutes) }}</th>{% endif -%}
107-
{%- if with_seconds %}<th>{{ form_label(form.seconds) }}</th>{% endif -%}
108-
</tr>
109-
</thead>
110-
<tbody>
111-
<tr>
112-
{%- if with_years %}<td>{{ form_widget(form.years) }}</td>{% endif -%}
113-
{%- if with_months %}<td>{{ form_widget(form.months) }}</td>{% endif -%}
114-
{%- if with_weeks %}<td>{{ form_widget(form.weeks) }}</td>{% endif -%}
115-
{%- if with_days %}<td>{{ form_widget(form.days) }}</td>{% endif -%}
116-
{%- if with_hours %}<td>{{ form_widget(form.hours) }}</td>{% endif -%}
117-
{%- if with_minutes %}<td>{{ form_widget(form.minutes) }}</td>{% endif -%}
118-
{%- if with_seconds %}<td>{{ form_widget(form.seconds) }}</td>{% endif -%}
119-
</tr>
120-
</tbody>
121-
</table>
122-
</div>
123-
{%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%}
124-
</div>
125-
{%- endif -%}
126-
{%- endblock dateinterval_widget -%}
127-
128-
{% block choice_widget_collapsed -%}
129-
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %}
130-
{{- parent() -}}
131-
{%- endblock %}
132-
133-
{% block choice_widget_expanded -%}
134-
{% if '-inline' in label_attr.class|default('') -%}
135-
{%- for child in form %}
136-
{{- form_widget(child, {
137-
parent_label_class: label_attr.class|default(''),
138-
translation_domain: choice_translation_domain,
139-
}) -}}
140-
{% endfor -%}
141-
{%- else -%}
142-
<div {{ block('widget_container_attributes') }}>
143-
{%- for child in form %}
144-
{{- form_widget(child, {
145-
parent_label_class: label_attr.class|default(''),
146-
translation_domain: choice_translation_domain,
147-
}) -}}
148-
{% endfor -%}
149-
</div>
150-
{%- endif %}
151-
{%- endblock choice_widget_expanded %}
15+
{%- endblock button_widget %}
15216

15317
{% block checkbox_widget -%}
15418
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{% use "bootstrap_4_layout.html.twig" %}
2+
3+
{# Labels #}
4+
5+
{% block form_label -%}
6+
{%- if label is same as(false) -%}
7+
<div class="{{ block('form_label_class') }}"></div>
8+
{%- else -%}
9+
{%- if expanded is not defined or not expanded -%}
10+
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label')|trim}) -%}
11+
{%- endif -%}
12+
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ block('form_label_class'))|trim}) -%}
13+
{{- parent() -}}
14+
{%- endif -%}
15+
{%- endblock form_label %}
16+
17+
{% block form_label_class -%}
18+
col-sm-2
19+
{%- endblock form_label_class %}
20+
21+
{# Rows #}
22+
23+
{% block form_row -%}
24+
{%- if expanded is defined and expanded -%}
25+
{{ block('fieldset_form_row') }}
26+
{%- else -%}
27+
<div class="form-group row{% if (not compound or force_error|default(false)) and not valid %} is-invalid{% endif %}">
28+
{{- form_label(form) -}}
29+
<div class="{{ block('form_group_class') }}">
30+
{{- form_widget(form) -}}
31+
{{- form_errors(form) -}}
32+
</div>
33+
{##}</div>
34+
{%- endif -%}
35+
{%- endblock form_row %}
36+
37+
{% block fieldset_form_row -%}
38+
<fieldset class="form-group">
39+
<div class="row{% if (not compound or force_error|default(false)) and not valid %} is-invalid{% endif %}">
40+
{{- form_label(form) -}}
41+
<div class="{{ block('form_group_class') }}">
42+
{{- form_widget(form) -}}
43+
{{- form_errors(form) -}}
44+
</div>
45+
</div>
46+
{##}</fieldset>
47+
{%- endblock fieldset_form_row %}
48+
49+
{% block submit_row -%}
50+
<div class="form-group row">
51+
<div class="{{ block('form_label_class') }}"></div>
52+
<div class="{{ block('form_group_class') }}">
53+
{{- form_widget(form) -}}
54+
</div>
55+
</div>
56+
{%- endblock submit_row %}
57+
58+
{% block reset_row -%}
59+
<div class="form-group row">
60+
<div class="{{ block('form_label_class') }}"></div>
61+
<div class="{{ block('form_group_class') }}">
62+
{{- form_widget(form) -}}
63+
</div>
64+
</div>
65+
{%- endblock reset_row %}
66+
67+
{% block form_group_class -%}
68+
col-sm-10
69+
{%- endblock form_group_class %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
{% use "bootstrap_base_layout.html.twig" %}
2+
3+
{# Widgets #}
4+
5+
{% block form_widget_simple -%}
6+
{% if type is not defined or type != 'hidden' %}
7+
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control' ~ (type|default('') == 'file' ? '-file' : ''))|trim}) -%}
8+
{% endif %}
9+
{{- parent() -}}
10+
{%- endblock form_widget_simple %}
11+
12+
{%- block widget_attributes -%}
13+
{%- if not valid %}
14+
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) %}
15+
{% endif -%}
16+
{{ parent() }}
17+
{%- endblock widget_attributes -%}
18+
19+
{% block button_widget -%}
20+
{%- set attr = attr|merge({class: (attr.class|default('btn-secondary') ~ ' btn')|trim}) -%}
21+
{{- parent() -}}
22+
{%- endblock button_widget %}
23+
24+
{% block checkbox_widget -%}
25+
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
26+
{%- set attr = attr|merge({class: attr.class|default('form-check-input')}) -%}
27+
{%- if 'checkbox-inline' in parent_label_class -%}
28+
{{- form_label(form, null, { widget: parent() }) -}}
29+
{%- else -%}
30+
<div class="form-check">
31+
{{- form_label(form, null, { widget: parent() }) -}}
32+
</div>
33+
{%- endif -%}
34+
{%- endblock checkbox_widget %}
35+
36+
{% block radio_widget -%}
37+
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
38+
{%- set attr = attr|merge({class: attr.class|default('form-check-input')}) -%}
39+
{%- if 'radio-inline' in parent_label_class -%}
40+
{{- form_label(form, null, { widget: parent() }) -}}
41+
{%- else -%}
42+
<div class="form-check">
43+
{{- form_label(form, null, { widget: parent() }) -}}
44+
</div>
45+
{%- endif -%}
46+
{%- endblock radio_widget %}
47+
48+
{% block choice_widget_expanded -%}
49+
{% if '-inline' in label_attr.class|default('') -%}
50+
{%- for child in form %}
51+
{{- form_widget(child, {
52+
parent_label_class: label_attr.class|default(''),
53+
translation_domain: choice_translation_domain,
54+
valid: valid,
55+
}) -}}
56+
{% endfor -%}
57+
{%- else -%}
58+
<div {{ block('widget_container_attributes') }}>
59+
{%- for child in form %}
60+
{{- form_widget(child, {
61+
parent_label_class: label_attr.class|default(''),
62+
translation_domain: choice_translation_domain,
63+
valid: valid,
64+
}) -}}
65+
{% endfor -%}
66+
</div>
67+
{%- endif %}
68+
{%- endblock choice_widget_expanded %}
69+
70+
{# Labels #}
71+
72+
{% block form_label -%}
73+
{%- if expanded is defined and expanded -%}
74+
{%- set element = 'legend' -%}
75+
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-legend')|trim}) -%}
76+
{%- endif -%}
77+
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-control-label')|trim}) -%}
78+
{{- parent() -}}
79+
{%- endblock form_label %}
80+
81+
{% block checkbox_radio_label -%}
82+
{#- Do not display the label if widget is not defined in order to prevent double label rendering -#}
83+
{%- if widget is defined -%}
84+
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-check-label')|trim}) -%}
85+
{%- if required -%}
86+
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) -%}
87+
{%- endif -%}
88+
{%- if parent_label_class is defined -%}
89+
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ parent_label_class)|trim}) -%}
90+
{%- endif -%}
91+
{%- if label is not same as(false) and label is empty -%}
92+
{%- if label_format is not empty -%}
93+
{%- set label = label_format|replace({
94+
'%name%': name,
95+
'%id%': id,
96+
}) -%}
97+
{%- else -%}
98+
{%- set label = name|humanize -%}
99+
{%- endif -%}
100+
{%- endif -%}
101+
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
102+
{{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans({}, translation_domain)) -}}
103+
</label>
104+
{%- endif -%}
105+
{%- endblock checkbox_radio_label %}
106+
107+
{# Rows #}
108+
109+
{% block form_row -%}
110+
{%- if expanded is defined and expanded -%}
111+
{%- set element = 'fieldset' -%}
112+
{%- endif -%}
113+
<{{ element|default('div') }} class="form-group">
114+
{{- form_label(form) -}}
115+
{{- form_widget(form) -}}
116+
{{- form_errors(form) -}}
117+
</{{ element|default('div') }}>
118+
{%- endblock form_row %}
119+
120+
{# Errors #}
121+
122+
{% block form_errors -%}
123+
{%- if errors|length > 0 -%}
124+
<div class="{% if form.parent %}invalid-feedback{% else %}alert alert-danger{% endif %}">
125+
<ul class="list-unstyled mb-0">
126+
{%- for error in errors -%}
127+
<li>{{ error.message }}</li>
128+
{%- endfor -%}
129+
</ul>
130+
</div>
131+
{%- endif %}
132+
{%- endblock form_errors %}

0 commit comments

Comments
 (0)