diff --git a/corehq/apps/app_execution/forms.py b/corehq/apps/app_execution/forms.py index 317500f3c30e..7e6e40f0d1db 100644 --- a/corehq/apps/app_execution/forms.py +++ b/corehq/apps/app_execution/forms.py @@ -57,7 +57,6 @@ def __init__(self, request, *args, **kwargs): if self.instance.id: self.fields["username"].initial = self.instance.django_user.username self.helper = hqcrispy.HQFormHelper() - self.helper.form_class = "form-horizontal" fields = [ "name", diff --git a/corehq/apps/commtrack/forms.py b/corehq/apps/commtrack/forms.py index 1a046e636136..f5b22eca8c73 100644 --- a/corehq/apps/commtrack/forms.py +++ b/corehq/apps/commtrack/forms.py @@ -4,22 +4,22 @@ from django.utils.translation import gettext_lazy from django.utils.html import format_html -from crispy_forms.bootstrap import PrependedText, StrictButton -from crispy_forms.helper import FormHelper +from crispy_forms.bootstrap import StrictButton from crispy_forms.layout import Fieldset, Layout from corehq.apps.consumption.shortcuts import ( get_default_monthly_consumption, set_default_consumption_for_product, ) -from corehq.apps.hqwebapp.widgets import BootstrapCheckboxInput +from corehq.apps.hqwebapp.crispy import FormActions, HQFormHelper from corehq.apps.products.models import SQLProduct class CommTrackSettingsForm(forms.Form): - use_auto_emergency_levels = forms.BooleanField(label='', required=False, widget=BootstrapCheckboxInput( - inline_label=gettext_lazy("Use default emergency levels") - )) + use_auto_emergency_levels = forms.BooleanField( + label=gettext_lazy("Use default emergency levels"), + required=False, + ) stock_emergency_level = forms.DecimalField( label=gettext_lazy("Emergency Level (months)"), required=False) @@ -28,22 +28,25 @@ class CommTrackSettingsForm(forms.Form): stock_overstock_threshold = forms.DecimalField( label=gettext_lazy("Overstock Level (months)"), required=False) - use_auto_consumption = forms.BooleanField(label='', required=False, widget=BootstrapCheckboxInput( - inline_label=gettext_lazy("Use automatic consumption calculation") - )) + use_auto_consumption = forms.BooleanField( + label=gettext_lazy("Use automatic consumption calculation"), + required=False, + ) consumption_min_transactions = forms.IntegerField( label=gettext_lazy("Minimum Transactions (Count)"), required=False) consumption_min_window = forms.IntegerField( label=gettext_lazy("Minimum Window for Calculation (Days)"), required=False) consumption_optimal_window = forms.IntegerField( label=gettext_lazy("Optimal Window for Calculation (Days)"), required=False) - individual_consumption_defaults = forms.BooleanField(label='', required=False, widget=BootstrapCheckboxInput( - inline_label=gettext_lazy("Configure consumption defaults individually by supply point") - )) + individual_consumption_defaults = forms.BooleanField( + label=gettext_lazy("Configure consumption defaults individually by supply point"), + required=False, + ) - sync_consumption_fixtures = forms.BooleanField(label='', required=False, widget=BootstrapCheckboxInput( - inline_label=gettext_lazy("Sync consumption fixtures") - )) + sync_consumption_fixtures = forms.BooleanField( + label=gettext_lazy("Sync consumption fixtures"), + required=False, + ) def clean(self): cleaned_data = super(CommTrackSettingsForm, self).clean() @@ -66,8 +69,7 @@ def clean(self): def __init__(self, *args, **kwargs): domain = kwargs.pop('domain') - self.helper = FormHelper() - self.helper.label_class = 'form-label' + self.helper = HQFormHelper() self.helper.layout = Layout( Fieldset( @@ -78,20 +80,22 @@ def __init__(self, *args, **kwargs): ), Fieldset( _('Consumption Settings'), - PrependedText('use_auto_consumption', ''), + 'use_auto_consumption', 'consumption_min_transactions', 'consumption_min_window', 'consumption_optimal_window', - PrependedText('individual_consumption_defaults', ''), + 'individual_consumption_defaults', ), Fieldset( _('Phone Settings'), - PrependedText('sync_consumption_fixtures', ''), + 'sync_consumption_fixtures', ), - StrictButton( - _("Submit"), - type="submit", - css_class='btn-primary', + FormActions( + StrictButton( + _("Submit"), + type="submit", + css_class='btn-primary', + ) ) ) @@ -112,9 +116,8 @@ class ConsumptionForm(forms.Form): def __init__(self, domain, *args, **kwargs): self.domain = domain super(ConsumptionForm, self).__init__(*args, **kwargs) - self.helper = FormHelper() + self.helper = HQFormHelper() self.helper.form_tag = False - self.helper.label_class = 'form-label' layout = [] products = SQLProduct.active_objects.filter(domain=domain) diff --git a/corehq/apps/commtrack/templates/commtrack/manage/default_consumption.html b/corehq/apps/commtrack/templates/commtrack/manage/default_consumption.html index 8e56d4a4dbb4..d183ea909e4b 100644 --- a/corehq/apps/commtrack/templates/commtrack/manage/default_consumption.html +++ b/corehq/apps/commtrack/templates/commtrack/manage/default_consumption.html @@ -21,8 +21,12 @@ {% trans 'Edit Default Monthly Consumption' %} {% crispy form %} - +
+
+ +
+
{% endblock %} diff --git a/corehq/apps/domain/forms.py b/corehq/apps/domain/forms.py index c24ecc4f92d9..51a61cafbc61 100644 --- a/corehq/apps/domain/forms.py +++ b/corehq/apps/domain/forms.py @@ -1273,7 +1273,6 @@ def __init__(self, domain, can_edit_eula, *args, user, **kwargs): ) self.helper = hqcrispy.HQFormHelper() - self.helper.form_class = "form-horizontal" self.helper.layout = crispy.Layout( crispy.Fieldset( _("Basic Information"), diff --git a/corehq/apps/email/forms.py b/corehq/apps/email/forms.py index ab42b4439117..dfe05200fb1e 100644 --- a/corehq/apps/email/forms.py +++ b/corehq/apps/email/forms.py @@ -115,12 +115,14 @@ def helper(self): hqcrispy.CheckboxField('use_tracking_headers'), crispy.Field('sns_secret'), crispy.Field('ses_config_set_name'), - twbscrispy.StrictButton( - _("Saved"), - type="submit", - css_class="btn-primary disable-on-submit", - data_bind="text: buttonText, enable: isFormChanged", - ), + hqcrispy.FormActions( + twbscrispy.StrictButton( + _("Saved"), + type="submit", + css_class="btn-primary disable-on-submit", + data_bind="text: buttonText, enable: isFormChanged", + ), + ) ) return helper diff --git a/corehq/apps/enterprise/forms.py b/corehq/apps/enterprise/forms.py index 4a36701cd3ff..a196e79555ae 100644 --- a/corehq/apps/enterprise/forms.py +++ b/corehq/apps/enterprise/forms.py @@ -7,9 +7,9 @@ from crispy_forms import layout as crispy from crispy_forms.bootstrap import PrependedText, StrictButton -from crispy_forms.helper import FormHelper from corehq.apps.accounting.utils import domain_has_privilege +from corehq.apps.hqwebapp import crispy as hqcrispy from corehq.apps.hqwebapp.widgets import BootstrapCheckboxInput from corehq.apps.export.models.export_settings import ExportFileType from corehq.privileges import DEFAULT_EXPORT_SETTINGS @@ -131,16 +131,15 @@ def __init__(self, *args, **kwargs): kwargs['initial'].update(self.export_settings.as_dict()) super(EnterpriseSettingsForm, self).__init__(*args, **kwargs) - self.helper = FormHelper(self) + self.helper = hqcrispy.HQFormHelper(self) self.helper.form_id = 'enterprise-settings-form' self.helper.form_action = reverse("edit_enterprise_settings", args=[self.domain]) - self.helper.label_class = 'form-label' self.helper.layout = crispy.Layout( crispy.Fieldset( _("Edit Enterprise Settings"), - PrependedText('restrict_domain_creation', ''), + hqcrispy.CheckboxField('restrict_domain_creation'), crispy.Div( - PrependedText('restrict_signup', '', data_bind='checked: restrictSignup'), + hqcrispy.CheckboxField('restrict_signup', data_bind='checked: restrictSignup'), ), crispy.Div( crispy.Field('restrict_signup_message'), @@ -157,29 +156,31 @@ def __init__(self, *args, **kwargs): crispy.Div( crispy.Field('forms_filetype'), ), - PrependedText('forms_auto_convert', ''), - PrependedText('forms_auto_format_cells', ''), - PrependedText('forms_expand_checkbox', ''), + hqcrispy.CheckboxField('forms_auto_convert'), + hqcrispy.CheckboxField('forms_auto_format_cells'), + hqcrispy.CheckboxField('forms_expand_checkbox'), ), crispy.Fieldset( _("Edit Default Case Export Settings"), crispy.Div( crispy.Field('cases_filetype') ), - PrependedText('cases_auto_convert', ''), + hqcrispy.CheckboxField('cases_auto_convert'), ), crispy.Fieldset( _("Edit Default OData Export Settings"), - PrependedText('odata_expand_checkbox', ''), + hqcrispy.CheckboxField('odata_expand_checkbox'), ), ) ) self.helper.layout.append( - StrictButton( - _("Update Enterprise Settings"), - type="submit", - css_class='btn-primary', + hqcrispy.FormActions( + StrictButton( + _("Update Enterprise Settings"), + type="submit", + css_class='btn-primary', + ) ) ) @@ -286,9 +287,8 @@ def __init__(self, *args, **kwargs): } super().__init__(*args, **kwargs) - self.helper = FormHelper(self) + self.helper = hqcrispy.HQFormHelper(self) self.helper.form_id = 'emw-settings-form' - self.helper.label_class = 'form-label' self.helper.layout = crispy.Layout( crispy.Fieldset( _("Manage Mobile Workers"), @@ -298,10 +298,12 @@ def __init__(self, *args, **kwargs): ), PrependedText('allow_custom_deactivation', ''), ), - StrictButton( - _("Update Settings"), - type="submit", - css_class='btn-primary', + hqcrispy.FormActions( + StrictButton( + _("Update Settings"), + type="submit", + css_class='btn-primary', + ) ) ) diff --git a/corehq/apps/export/forms.py b/corehq/apps/export/forms.py index 00d81a5cf988..255b43db9536 100644 --- a/corehq/apps/export/forms.py +++ b/corehq/apps/export/forms.py @@ -8,7 +8,7 @@ import dateutil from crispy_forms import layout as crispy -from crispy_forms.helper import FormHelper +from corehq.apps.hqwebapp import crispy as hqcrispy from crispy_forms.bootstrap import StrictButton from corehq import privileges @@ -1078,7 +1078,7 @@ class DatasourceExportDownloadForm(forms.Form): def __init__(self, domain, *args, **kwargs): super(DatasourceExportDownloadForm, self).__init__(*args, **kwargs) - self.helper = FormHelper() + self.helper = HQFormHelper() self.fields['data_source'].choices = self.domain_datasources(domain) @@ -1094,11 +1094,13 @@ def __init__(self, domain, *args, **kwargs): data_bind='visible: haveDatasources' ), ), - StrictButton( - _("Download Data Export Tool query file"), - type="submit", - css_class="btn-primary", - data_bind="enable: haveDatasources" + hqcrispy.FormActions( + StrictButton( + _("Download Data Export Tool query file"), + type="submit", + css_class="btn-primary", + data_bind="enable: haveDatasources" + ), ), ) diff --git a/corehq/apps/hqwebapp/crispy.py b/corehq/apps/hqwebapp/crispy.py index fceb944f7fa9..f86d05b17762 100644 --- a/corehq/apps/hqwebapp/crispy.py +++ b/corehq/apps/hqwebapp/crispy.py @@ -1,4 +1,3 @@ -import re from contextlib import contextmanager from django.forms.widgets import DateTimeInput from django.template.loader import render_to_string @@ -13,11 +12,9 @@ from crispy_forms.layout import LayoutObject from crispy_forms.utils import flatatt, get_template_pack, render_field -CSS_LABEL_CLASS = 'col-xs-12 col-sm-4 col-md-4 col-lg-2' -CSS_LABEL_CLASS_BOOTSTRAP5 = 'col-xs-12 col-sm-4 col-md-4 col-lg-3' -CSS_FIELD_CLASS = 'col-xs-12 col-sm-8 col-md-8 col-lg-6' -CSS_FIELD_CLASS_BOOTSTRAP5 = 'col-xs-12 col-sm-8 col-md-8 col-lg-9' -CSS_ACTION_CLASS = CSS_FIELD_CLASS + ' col-sm-offset-4 col-md-offset-4 col-lg-offset-2' +CSS_LABEL_CLASS = 'field-label' +CSS_FIELD_CLASS = 'field-control' +CSS_ACTION_CLASS = CSS_FIELD_CLASS class HQFormHelper(FormHelper): @@ -28,13 +25,7 @@ class HQFormHelper(FormHelper): def __init__(self, *args, **kwargs): super(HQFormHelper, self).__init__(*args, **kwargs) from corehq.apps.hqwebapp.utils.bootstrap import get_bootstrap_version, BOOTSTRAP_5 - bootstrap_version = get_bootstrap_version() - use_bootstrap5 = bootstrap_version == BOOTSTRAP_5 - if use_bootstrap5: - self.label_class = CSS_LABEL_CLASS_BOOTSTRAP5 - self.field_class = CSS_FIELD_CLASS_BOOTSTRAP5 - self.use_bootstrap5 = use_bootstrap5 - self.form_class = 'form' + self.use_bootstrap5 = get_bootstrap_version() == BOOTSTRAP_5 if 'autocomplete' not in self.attrs: self.attrs.update({ @@ -76,15 +67,7 @@ class ErrorsOnlyField(Field): def get_form_action_class(): - """This is only valid for bootstrap 5""" - return CSS_LABEL_CLASS_BOOTSTRAP5.replace('col', 'offset') + ' ' + CSS_FIELD_CLASS_BOOTSTRAP5 - - -def _get_offsets(context): - label_class = context.get('label_class', '') - use_bootstrap5 = context.get('use_bootstrap5') - return (label_class.replace('col', 'offset') if use_bootstrap5 - else re.sub(r'(xs|sm|md|lg)-', r'\g<1>-offset-', label_class)) + return CSS_FIELD_CLASS class FormActions(OriginalFormActions): @@ -102,11 +85,9 @@ def render(self, form, context, template_pack=None, **kwargs): template_pack=template_pack, ) fields_html = mark_safe(fields_html) # nosec: just concatenated safe fields - offsets = _get_offsets(context) context.update({ 'formactions': self, 'fields_output': fields_html, - 'offsets': offsets, 'field_class': context.get('field_class', '') }) return render_to_string(self.template, context.flatten()) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/less/_hq/includes/extensions.less b/corehq/apps/hqwebapp/static/hqwebapp/less/_hq/includes/extensions.less new file mode 100644 index 000000000000..80394fb674dc --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/less/_hq/includes/extensions.less @@ -0,0 +1,16 @@ +.field-label { + &:extend(.col-xs-12, .col-sm-4, .col-md-4, .col-lg-2); +} + +.field-label + .field-control { + &:extend(.col-xs-12, .col-sm-8, .col-md-8, .col-lg-6); +} + +:not(.field-label) + .field-control, +.form-actions > .field-control:first-child, +.form-group > .field-control:first-child { + &:extend( + .col-sm-offset-4, .col-md-offset-4, .col-lg-offset-2, + .col-xs-12, .col-sm-8, .col-md-8, .col-lg-6 + ); +} diff --git a/corehq/apps/hqwebapp/static/hqwebapp/less/bootstrap.less b/corehq/apps/hqwebapp/static/hqwebapp/less/bootstrap.less index 29e6b786ecdd..f801d1b4b988 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/less/bootstrap.less +++ b/corehq/apps/hqwebapp/static/hqwebapp/less/bootstrap.less @@ -9,6 +9,7 @@ // Core variables and mixins @import "_hq/includes/variables.less"; @import "_hq/includes/mixins.less"; +@import "_hq/includes/extensions.less"; // Reset and dependencies @import "../../../../../../node_modules/bootstrap/less/normalize.less"; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_forms.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_forms.scss index 481acf947bd2..2f7aef91a69d 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_forms.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_forms.scss @@ -1,3 +1,16 @@ +.field-label { + @extend .col-form-label, .col-12, .col-sm-4, .col-md-3, .col-lg-2; +} + +.field-label + .field-control { + @extend .col-12, .col-sm-8, .col-md-9, .col-lg-10; +} + +:not(.field-label) + .field-control, +.row > .field-control:first-child { + @extend .offset-sm-4, .offset-md-3, .offset-lg-2; +} + .row > div > .form-check:first-child, .row > div > .input-group > .form-check:first-child { padding-top: add($input-padding-y, $input-border-width); diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap3/form_actions.html b/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap3/form_actions.html index 00f51a85d933..1849503aa48d 100644 --- a/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap3/form_actions.html +++ b/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap3/form_actions.html @@ -1,5 +1,5 @@
-
+
{{ fields_output }}
diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap5/form_actions.html b/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap5/form_actions.html index fe52406f6134..1c56acebb3e6 100644 --- a/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap5/form_actions.html +++ b/corehq/apps/hqwebapp/templates/hqwebapp/crispy/bootstrap5/form_actions.html @@ -1,5 +1,5 @@
-
+
{{ fields_output }}
diff --git a/corehq/apps/hqwebapp/templatetags/hq_shared_tags.py b/corehq/apps/hqwebapp/templatetags/hq_shared_tags.py index 70533be19c8f..c17a36543abc 100644 --- a/corehq/apps/hqwebapp/templatetags/hq_shared_tags.py +++ b/corehq/apps/hqwebapp/templatetags/hq_shared_tags.py @@ -246,19 +246,13 @@ def can_use_restore_as(request): @register.simple_tag def css_label_class(): - from corehq.apps.hqwebapp.crispy import CSS_LABEL_CLASS, CSS_LABEL_CLASS_BOOTSTRAP5 - from corehq.apps.hqwebapp.utils.bootstrap import get_bootstrap_version, BOOTSTRAP_5 - if get_bootstrap_version() == BOOTSTRAP_5: - return CSS_LABEL_CLASS_BOOTSTRAP5 + from corehq.apps.hqwebapp.crispy import CSS_LABEL_CLASS return CSS_LABEL_CLASS @register.simple_tag def css_field_class(): - from corehq.apps.hqwebapp.crispy import CSS_FIELD_CLASS, CSS_FIELD_CLASS_BOOTSTRAP5 - from corehq.apps.hqwebapp.utils.bootstrap import get_bootstrap_version, BOOTSTRAP_5 - if get_bootstrap_version() == BOOTSTRAP_5: - return CSS_FIELD_CLASS_BOOTSTRAP5 + from corehq.apps.hqwebapp.crispy import CSS_FIELD_CLASS return CSS_FIELD_CLASS diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/hqwebapp/crispy/form_actions.html.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/hqwebapp/crispy/form_actions.html.diff.txt index ffe8bfd29c8e..c719c0d4debd 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/hqwebapp/crispy/form_actions.html.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/hqwebapp/crispy/form_actions.html.diff.txt @@ -2,11 +2,11 @@ +++ @@ -1,5 +1,5 @@ -
--
+-
- {{ fields_output }} -
+
-+
++
+ {{ fields_output }} +
diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/forms._forms.style.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/forms._forms.style.diff.txt index 920cf155f13e..d123abd6edd2 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/forms._forms.style.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/stylesheets/imports/forms._forms.style.diff.txt @@ -1,20 +1,19 @@ --- +++ -@@ -1,356 +1,7 @@ +@@ -1,356 +1,20 @@ -// FORM ACTIONS from TWBS 2 -// ------------ - -[ng\:cloak],[ng-cloak],.ng-cloak{ - display:none !important --} -- ++.field-label { ++ @extend .col-form-label, .col-12, .col-sm-4, .col-md-3, .col-lg-2; + } + -.form-actions { - padding: (@line-height-base * @font-size-base - 1px) 0px @line-height-base * @font-size-base; - margin-top: @line-height-base * 1em; -+.row > div > .form-check:first-child, -+.row > div > .input-group > .form-check:first-child { -+ padding-top: add($input-padding-y, $input-border-width); - margin-bottom: 0; +- margin-bottom: 0; - background-color: @navbar-default-bg; - border-top: 1px solid @legend-border-color; - .border-bottom-radius(@border-radius-base); @@ -31,12 +30,17 @@ - margin-left: 0; - } - } --} -- ++.field-label + .field-control { ++ @extend .col-12, .col-sm-8, .col-md-9, .col-lg-10; + } + -.form-horizontal .control-label { - text-align: left; // override bootstrap --} -- ++:not(.field-label) + .field-control, ++.row > .field-control:first-child { ++ @extend .offset-sm-4, .offset-md-3, .offset-lg-2; + } + -legend .subtext { - font-size: .8em; - color: lighten(@cc-text, 40%); @@ -357,10 +361,14 @@ - -.controls-text { - padding-top: 7px; ++.row > div > .form-check:first-child, ++.row > div > .input-group > .form-check:first-child { ++ padding-top: add($input-padding-y, $input-border-width); ++ margin-bottom: 0; } .form-hide-actions .form-actions { -@@ -361,21 +12,34 @@ +@@ -361,21 +25,34 @@ .validationMessage { display: block; padding-top: 8px; diff --git a/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/crispy.md b/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/crispy.md index d0a43896efe3..0bc4db071091 100644 --- a/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/crispy.md +++ b/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/crispy.md @@ -6,9 +6,6 @@ section of the style guide. A few useful things to know about crispy forms in Bootstrap 5: -* Checkboxes, typically based on a `BooleanField`, need to be updated to use the `BootstrapCheckboxInput` as their -`widget`, as shown in this style guide example. * As described in this section of the style guide, best practice is to use one of HQ's standard helper classes for layout. Doing so means you can delete form_class, label_class, diff --git a/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/css-form-group.md b/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/css-form-group.md index 7b6ac9575df2..b16cdcaa9098 100644 --- a/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/css-form-group.md +++ b/corehq/apps/hqwebapp/utils/bootstrap/changes_guide/css-form-group.md @@ -1,10 +1,10 @@ `form-group` has been dropped. Use grid utilities instead. -Since we are opting for vertical forms (where the label is directly above the field), take the following actions: +Take the following actions: * Remove the `div` wrapper from the `form-group`'s first child, which contains the field's label. -* Remove the column classes (`col-lg-2`, etc.) from the `form-group`'s first child, usually a `