The Goal
When a store has customers that are businesses or institutional, the standard Address field properties and formatting may not align well with actual billing and shipping addresses. For example, a business address may not include the customer's first and last name as the first line; instead, an organizational name may be used. If a contact name is included, it may be an "Attention" line. Additionally, businesses may require a line with reference or purchase order number.
Ultimately, what we want is an Address field with increased flexibility for B2B customers, without losing the powerful formatting functionality provided by the Drupal Address module (included in Commerce Core).
Note that this work was done for an English-language website, so multilingual issues were not taken into consideration. Additionally, changing the structure of customer addresses can create problems with integrations that expect data in a specific format such as payment gateways or fraud detection services. For that reason, I categorized the level-of-effort for this customization as High; the custom code is straightforward, but making the change may cause unexpected consequences. In a follow-up article, I'll describe a fix for an Authorize.net integration that "broke" as a result of the B2B Address customization.
A Solution
We will provide a customized version of the standard Address field type with properties formatted as follows:
- Organization (required)
- instead of "First name"
- Attention (optional, automatically prepended with "ATTN:"
- instead of "Middle name" (which is typically hidden by default)
- Reference/PO # (optional)
- instead of "Last name"
- Additional Line (optional)
- instead of "Company"
- Address Line 1 (required)
- instead of "Street address"
- Address Line 2 (optional)
- instead of "Street address line 2"
- Dynamically formatted properties based on selected Country: dependent locality, locality, administrative area, postal code, and country.


Implementation:
The Address module provides an address format event that we can use to customize our B2B addresses: AddressEvents::ADDRESS_FORMAT
. We can create a custom event subscriber to rearrange the components of our addresses. You can read more about address formats in the Commerce Address Formats documentation.
In our custom code, we pull the additionalName
and familyName
placeholders out of the format string and then replace the givenName
placeholder with given/additional/family names, separated by newline characters:
public static function getSubscribedEvents() {
$events[AddressEvents::ADDRESS_FORMAT][] = ['onAddressFormat'];
return $events;
}
/**
* Alters the address format.
*
* @param \Drupal\address\Event\AddressFormatEvent $event
* The address format event.
*/
public function onAddressFormat(AddressFormatEvent $event) {
$definition = $event->getDefinition();
// Place %additionalName after %givenName in the format.
$format = $definition['format'];
$format = str_replace('%additionalName', '', $format);
$format = str_replace('%familyName', '', $format);
$format = str_replace('%givenName', "%givenName\n%additionalName\n%familyName", $format);
$definition['format'] = $format;
$event->setDefinition($definition);
}
If we look at the address form and formatted address at this point, we've made progress but still have a couple problems:
- Incorrect form element labels
- Incorrect "required" form elements


The second problem is quite easy to fix. Just manage the Address field settings to override the default configuration:

For the form field labels, a variety of approaches are possible, including form alter hooks, but instead, we use a custom "B2B address" field widget plugin and a custom "B2B address" form element. For the custom form element, we override the addressElements
method in the Address
form element class so that we can re-label and re-size each of the changed address components, like this:
<?php
namespace Drupal\my_module\Element;
use Drupal\address\Element\Address;
/**
* Provides a B2B address form element.
*
* @FormElement("my_module_b2b_address")
*/
class B2BAddress extends Address {
/**
* {@inheritdoc}
*/
protected static function addressElements(array $element, array $value) {
$element = parent::addressElements($element, $value);
$context = ['context' => 'Address label'];
$element['given_name']['#title'] = t('Organization', [], $context);
$element['given_name']['#size'] = 60;
$element['additional_name']['#title'] = t('Attention', [], $context);
$element['additional_name']['#size'] = 60;
$element['family_name']['#title'] = t('Ref/PO #', [], $context);
$element['family_name']['#size'] = 60;
$element['address_line1']['#title'] = t('Address Line 1', [], $context);
$element['address_line2']['#title'] = t('Address Line 2', [], $context);
$element['organization']['#title'] = t('Additional Line', [], $context);
return $element;
}
}
The custom B2B field widget is a simple override of the default address widget, to use the custom form element instead of the of the default one:
<?php
namespace Drupal\my_module\Plugin\Field\FieldWidget;
use Drupal\address\Plugin\Field\FieldWidget\AddressDefaultWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/** doc block omitted */
class B2BAddressWidget extends AddressDefaultWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$element['address']['#type'] = 'my_module_b2b_address';
return $element;
}
}
Lastly, for the "ATTN:" detail described in the Solution above, we have a preprocess hook in our theme for the address field on both the customer and shipping profiles:
function my_theme_preprocess_field__profile__address(&$variables, $hook) {
$content = &$variables['items'][0]['content'];
$element = $variables['element'];
if (in_array($element['#bundle'], ['customer', 'shipping'])) {
// Add prefix to the additional_name field.
if (!empty($content['additional_name']['#value'])) {
$content['additional_name']['#value'] = t('ATTN: @name', [
'@name' => $content['additional_name']['#value']
]);
}
}
}
Comments