Documentation Home

i18n Enterprise: Dynamic Regional Forms

DynamicRegionalForm

The Dynamic Regional Form entity allows maintenance of Internationalized forms based on Country.
For example, the fields for an Irish address form would be different than the fields for an address form for France.
In this case, Ireland does not contain a Postal Code. An admin will have the ability to maintain and modify the layout
for these country-based forms through the Broadleaf Admin.

To get started, let's create a customer billing form and a customer shipping form using Dynamic Regional Forms with the following SQL.

INSERT INTO BLC_DYNAMIC_REGIONAL_FORM (FORM_ID, CREATED_BY, DATE_CREATED, DATE_UPDATED, UPDATED_BY, COUNTRY, FORM_TYPE) VALUES (-29000, 1, NULL, NULL, NULL, 'US', 'BILLING_FORM');
INSERT INTO BLC_DYN_REG_FORM_FIELD_CAT (FORM_ID, FIELD_CAT_TYPE) VALUES (-29000, 'ADDRESS');
INSERT INTO BLC_DYNAMIC_REGIONAL_FORM (FORM_ID, CREATED_BY, DATE_CREATED, DATE_UPDATED, UPDATED_BY, COUNTRY, FORM_TYPE) VALUES (-29200, 1, NULL, NULL, NULL, 'US', 'SHIPPING_FORM');
INSERT INTO BLC_DYN_REG_FORM_FIELD_CAT (FORM_ID, FIELD_CAT_TYPE) VALUES (-29200, 'ADDRESS');

The above SQL script creates the billing and shipping forms for the USA locale. Next, we will add Dynamic Regional Form Fields.

-- US Billing Form Fields
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29000, NULL, 'Full Name', 1, 'FULL_NAME', 1, -29000);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29001, NULL, 'Address', 2, 'ADDRESS_LINE_1', 1, -29000);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, FIELD_GROUP, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29002, NULL, 'City', 3, 'CITY_COUNTRY_SUB_POSTAL', 'CITY', 1, -29000);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, FIELD_GROUP, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29003, NULL, NULL, 4, 'CITY_COUNTRY_SUB_POSTAL', 'STATE_LOOKUP', 1, -29000);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, FIELD_GROUP, TYPE, IS_REQUIRED, REG_EX, FORM_ID) VALUES (-29004, NULL, 'Zip', 5, 'CITY_COUNTRY_SUB_POSTAL', 'POSTAL_CODE', 1,'^\\d{5}(-\\d{4})?$', -29000);

 -- US Shipping Form Fields
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29200, NULL, 'Full Name', 1, 'FULL_NAME', 1, -29200);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29201, NULL, 'Address', 2, 'ADDRESS_LINE_1', 1, -29200);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, FIELD_GROUP, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29202, NULL, 'City', 3, 'CITY_COUNTRY_SUB_POSTAL','CITY', 1, -29200);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, FIELD_GROUP, TYPE, IS_REQUIRED, FORM_ID) VALUES (-29203, NULL, NULL, 4, 'CITY_COUNTRY_SUB_POSTAL','STATE_LOOKUP', 1, -29200);
INSERT INTO BLC_DYN_REG_FORM_FIELD (FIELD_ID, FIELD_VALUE, PLACEHOLDER, SEQUENCE, FIELD_GROUP, TYPE, IS_REQUIRED, REG_EX, FORM_ID) VALUES (-29204, NULL, 'Zip', 5, 'CITY_COUNTRY_SUB_POSTAL','POSTAL_CODE', 1,'^\\d{5}(-\\d{4})?$', -29200);

The above SQL script also specifies each field's placeholder value, the field's type, the field's owning form, a regEx used for validation, the field's group, and whether or not the field is required. These forms, as well as their associated fields, can be viewed and edited through the Broadleaf Admin.

Usage Example

Let's look at an example:

    <blc:dynamic_regional_form th:object="${billingInfoForm}"
              th:action="@{/checkout/billing}"
              th:country="${billingInfoForm.address.isoCountryAlpha2?.alpha2}"
              name="BILLING_FORM"
              method="post" id="billing_info"
              novalidate="novalidate">

            <div class="form50">
                <ul id="billing_info_country_nonjs">
                    <li>- Please Select a Country -</li>
                    <li th:each="country : ${countries}">
                        <a th:href="@{/checkout/billing-country(blDynamicFormCountryCode=${country.alpha2})}"
                           th:text="${country.name + ' (' + country.alpha2 + ')'}"></a>
                    </li>
                </ul>
                <select id="billing_info_country"
                        class="dynamic_address_country required cloneable"
                        name="address.isoCountryAlpha2"
                        th:classappend="${#fields.hasErrors('address.isoCountryAlpha2')}? 'fieldError'">
                    <option value="" th:attr="data-href=@{/checkout}">- Please Select a Country -</option>
                    <option th:each="country : ${countries}"
                            th:if="${country.name}"
                            th:value="${country.alpha2}"
                            th:attr="data-href=@{/checkout/billing-country(blDynamicFormCountryCode=${country.alpha2})}"
                            th:selected="${country.alpha2.equals(blDynamicCountry)}"
                            th:text="${country.name + ' (' + country.alpha2+ ')'}">
                    </option>
                </select>
            </div>

        <div class="clearfix"></div>

            <div th:if="${blDynamicRegionalForm}" id="dynamic-billing-form">
                <span class="error" th:if="${#fields.hasErrors('*')}" th:errors="*"></span>
                <div th:each="fieldGroup : ${blFieldGroups}">
                    <div class="clearfix"></div>
                    <div class="form50">
                    <span th:each="fieldItem : ${fieldGroup.value}">
                        <span th:if="${#strings.isEmpty(fieldItem.type.lookupCategory) == false}">
                            <select class="dynamic_address_country_sub_31 required cloneable"
                                       th:field="*{__${fieldItem.type.modelAttributeName}__}">
                                <option th:if="${#strings.isEmpty(__${fieldItem.type.lookupCategory}__)}" value="">--</option>
                                <option th:each="subdivision : ${__${fieldItem.type.lookupCategory}__}"
                                          th:value="${subdivision.alternateAbbreviation}"
                                          th:text="${subdivision.name}">
                                </option>
                            </select>
                         </span>
                         <span th:if="${#strings.isEmpty(fieldItem.type.lookupCategory) == true}">
                            <input type="text"
                                   class=" field31 cloneable"
                                   th:classappend="${#fields.hasErrors('__${fieldItem.type.modelAttributeName}__')}? fieldError"
                                   th:placeholder="${fieldItem.placeHolder}"
                                   th:field="*{__${fieldItem.type.modelAttributeName}__}"
                              />
                        </span>
                   </span>
                   </div>
                   .............
                </div>
            </div>

        <div class="clearfix"></div>
    </blc:dynamic_regional_form>

When the Dynamic Regional Form processor executes, the first thing it will do is look for an ISOCountry to render the proper form. If it doesn't find a dynamic form associated with the ISOCountry, then it will render whatever is inside the page element block. The ISOCountry can be determined in a number of ways:

  1. The ISOCountry is passed into the processor.
  2. If no ISOCountry is passed in, then the ISOCountry will be retrieved from the Dynamic Regional Form that was passed into the processor.
  3. If neither an ISOCountry or a Dynamic Regional Form were passed to the processor, then the processor will default the ISOCountry to the user's locale.

Since one of the main use cases for this module is to render internationalized address forms, this module includes two out-of-the-box controllers to handle the Billing and Shipping address forms for checkout. These controllers provide endpoints to manage the ISOCountry context for each form. You can see the example above:

<li th:each="country : ${countries}">
    <a th:href="@{/checkout/billing-country(blDynamicForm=billing_info_form,blDynamicFormCountryCode=${country.abbreviation})}"
       th:text="${country.name + ' (' + country.abbreviation + ')'}"></a>
</li>

The processor will populate a list of countries and put them in the element countries. This list is used to
select the country that the processor should use when retrieving the associated Dynamic Form.

<blc:dynamic_regional_form th:object="${billingInfoForm}"
    th:action="@{/checkout/billing}"
    th:country="${billingInfoForm.address.isoCountryAlpha2?.alpha2}"
    name="BILLING_FORM"
    method="post" id="billing_info"
    novalidate="novalidate">

In the example above, we choose to render a Dynamic Regional Form of that has a type name of BILLING_FORM. We also
specified the model object used is BillingInfoForm and the ISOCountry for the form.

<div th:each="fieldGroup : ${blFieldGroups}">
   <div class="clearfix"></div>
   <div class="form50">
   <span th:each="fieldItem : ${fieldGroup.value}">

The processor also makes a collection of fieldGroups and places the collection in the element variable blFieldGroups. Each field group contains all the DynamicRegionalFormFields that are associated with it. This provides a way to specify multiple fields that are included in one div element (for instance Postal Code, City, and State for U.S.).

<span th:if="${#strings.isEmpty(fieldItem.type.lookupCategory) == false}">
    <select class="dynamic_address_country_sub_31 required cloneable"
            th:field="*{__${fieldItem.type.modelAttributeName}__}">
        <option th:if="${#strings.isEmpty(__${fieldItem.type.lookupCategory}__)}" value="">--</option>
        <option th:each="subdivision : ${__${fieldItem.type.lookupCategory}__}"
            th:value="${subdivision.alternateAbbreviation}"
            th:text="${subdivision.name}">
        </option>
    </select>
</span>

Each DynamicRegionalFormField is of a specific DynamicFormFieldType.
The field type specifies if there is a lookupCategory and it specifies the modelAttributeName. A lookup Category
is used to specify what collection should be pulled from the database and made available. This lookup gathers all CountrySubdivisions with a CountrySubdivisionCategory for the specified lookupCategory type. For
example: the field has a lookup category of STATE_LOOKUP. The processor will pull all the CountrySubdivisions for the passed
in ISOCountry that have a Country Subdivision Category of type State. The states that are available
for that particular country will be placed on an element with the same name (in this case, the element variable
name would be "STATE_LOOKUP"). We also use the modelAttributeName and the Thymeleaf attribute th:field to
bind our input to a particular property. The expression, ${fieldItem.type.modelAttributeName}, evaluates to
address.stateProvinceRegion. If the field does not specify a lookup category, then the following logic would apply.

<span th:if="${#strings.isEmpty(fieldItem.type.lookupCategory) == true}">
    <input type="text"
         class=" field31 cloneable"
         th:classappend="${#fields.hasErrors('__${fieldItem.type.modelAttributeName}__')}? fieldError"
         th:placeholder="${fieldItem.placeHolder}"
         th:field="*{__${fieldItem.type.modelAttributeName}__}"/>
</span>

In the above snippet, the template would render a standard form text input element. We would still use
modelAttributeName and th:field to bind the input. We also choose to bind our placeholder value to the element
in the example above. Two other properties/attributes that you could choose to bind (if applicable) are a default value and a label.

Validation of the above address forms is handled by BroadleafCommonAddressValidatorExtensionHandler. The validator will check each Dynamic Regional Form Field according to whether or not the field is required and validate the field's value against the specified regular expression (regEx), if one is given.