Using Drupal 8's Address Field in Custom Forms

James Nettik

In Drupal 7, the Address Field module provided developers an easy way to collect complex address information with relative ease. You could simply add the field to your content type and configure which countries you support along with what parts of an address are needed. However, this ease was limited to fieldable entities. If you needed to collect address information somewhere that wasn’t a fieldable entity, you had a lot more work in store for you. Chances are good that the end result would be as few text fields as possible, no validation, and only supporting with a single country. If you were feeling ambitious, maybe you would have provided a select list with the states or provinces provided via a hardcoded array.

During my most recent Drupal 8 project I wanted to collect structured address information outside the context of an entity. Specifically, I wanted to add a section for address and phone number to the Basic Site Settings configuration page. As it turns out, the same functionality you get on entities is now also available to the Form API.

Address Field’s port to Drupal 8 came in the form of a whole new module, the Address module. With it comes a new address form element. Let’s use that to add a “Site Address” field to the Basic Settings. First we’ll implement hook_form_FORM_ID_alter() in a custom module’s .module file:

use Drupal\Core\Form\FormStateInterface;
 
function MYMODULE_form_system_site_information_settings_alter(&$form, FormStateInterface $form_state) {
  // Overrides go here...
}

Don’t forget to add use Drupal\Core\Form\FormStateInterface; at the top of your file. Next, we’ll add a details group and a fieldset for the address components to go into:

function MYMODULE_form_system_site_information_settings_alter(&$form, FormStateInterface $form_state) {
  // Create our contact information section.
  $form['site_location'] = [
    '#type' => 'details',
    '#title' => t('Site Location'),
    '#open' => TRUE,
  ];
 
  $form['site_location']['address'] = [
    '#type' => 'fieldset',
    '#title' => t('Address'),
  ];
}

Once the fieldset is in place, we can go ahead and add the address components. To do that you’ll first need to install the Address module and its dependencies. You’ll also need to add use CommerceGuys\Addressing\AddressFormat\AddressField; at the top of the file as we’ll need some of the constants defined there later.

use Drupal\Core\Form\FormStateInterface;
use CommerceGuys\Addressing\AddressFormat\AddressField;
 
function MYMODULE_form_system_site_information_settings_alter(&$form, FormStateInterface $form_state) {
  // … detail and fieldset code …
 
  // Create the address field.
  $form['site_location']['address']['site_address'] = [
    '#type' => 'address',
    '#default_value' => ['country_code' => 'US'],
    '#used_fields' => [
      AddressField::ADDRESS_LINE1,
      AddressField::ADDRESS_LINE2,
      AddressField::ADMINISTRATIVE_AREA,
      AddressField::LOCALITY,
      AddressField::POSTAL_CODE,
    ],
    '#available_countries' => ['US'],
  ];
}

There’s a few things we’re doing here worth going over. First we set '#type' => 'address', which the Address module creates for us. Next we set a #default_value for country_code to US. That way the United States specific field config is displayed when the page loads.

The #used_fields key allows us to configure which address information we want to collect. This is done by passing an array of constants as defined in the AddressField class. The full list of options is:

AddressField::ADMINISTRATIVE_AREA
AddressField::LOCALITY
AddressField::DEPENDENT_LOCALITY
AddressField::POSTAL_CODE
AddressField::SORTING_CODE
AddressField::ADDRESS_LINE1
AddressField::ADDRESS_LINE2
AddressField::ORGANIZATION
AddressField::GIVEN_NAME
AddressField::ADDITIONAL_NAME
AddressField::FAMILY_NAME

Without any configuration, a full address field looks like this when displaying addresses for the United States.

For our example above, we only needed the street address (ADDRESS_LINE1 and ADDRESS_LINE2), city (LOCALITY), state (ADMINISTRATIVE_AREA), and zip code (POSTAL_CODE).

Lastly, we define which countries we will be supporting. This is done by passing an array of country codes into the #available_countries key. For our example we only need addresses from the United States, so that’s the only value we pass in.

The last step in our process is saving the information to the Basic Site Settings config file. First we need to add a new submit handler to the form. At the end of our hook, let’s add this:

function MYMODULE_form_system_site_information_settings_alter(&$form, FormStateInterface $form_state) {
  // … detail and fieldset code …
 
  // … address field code …
 
  // Add a custom submit handler for our new values.
  $form['#submit'][] = 'MYMODULE_site_address_submit';
}

Now we’ll create the handler:

/**
* Custom submit handler for our address settings.
*/
function MYMODULE_site_address_submit($form, FormStateInterface $form_state) {
  \Drupal::configFactory()->getEditable('system.site')
    ->set(‘address’, $form_state->getValue('site_address'))
    ->save();
}

This loads our site_address field from the submitted values in $form_state, and saves it to the system.site config. The exported system.site.yml file should now look something like:

name: 'My Awesome Site'
mail: test@domain.com
slogan: ''
page:
 403: ''
 404: ''
 front: /user/login
admin_compact_mode: false
weight_select_max: 100
langcode: en
default_langcode: en
address:
 country_code: US
 langcode: ''
 address_line1: '123 W Elm St.'
 address_line2: ''
 locality: Denver
 administrative_area: CO
 postal_code: '80266'
 given_name: null
 additional_name: null
 family_name: null
 organization: null
 sorting_code: null
 dependent_locality: null

After that, we need to make sure our field will use the saved address as the #default_value. Back in our hook, let’s update that key with the following:

function MYMODULE_form_system_site_information_settings_alter(&$form, FormStateInterface $form_state) {
  // … detail and fieldset code …
 
  // Create the address field.
  $form['site_location']['address']['site_address'] = [
    '#type' => 'address',
    '#default_value' => \Drupal::config('system.site')->get('address') ?? [
      'country_code' => 'US',
    ],
    '#used_fields' => [
      AddressField::ADDRESS_LINE1,
      AddressField::ADDRESS_LINE2,
      AddressField::ADMINISTRATIVE_AREA,
      AddressField::LOCALITY,
      AddressField::POSTAL_CODE,
    ],
    '#available_countries' => ['US'],
  ];
 
  // … custom submit handler ...
}

Using PHP 7’s null coalesce operator, we either set the default to the saved values or to a sensible fallback if nothing has been saved yet. Putting this all together, our module file should now look like this:

<?php
 
/**
 * @file
 * Main module file.
 */
 
use Drupal\Core\Form\FormStateInterface;
use CommerceGuys\Addressing\AddressFormat\AddressField;
 
/**
 * Implements hook_form_ID_alter().
 */
function MYMODULE_form_system_site_information_settings_alter(&$form, FormStateInterface $form_state) {
  // Create our contact information section.
  $form['site_location'] = [
    '#type' => 'details',
    '#title' => t('Site Location'),
    '#open' => TRUE,
  ];
 
  $form['site_location']['address'] = [
    '#type' => 'fieldset',
    '#title' => t('Address'),
  ];
 
  // Create the address field.
  $form['site_location']['address']['site_address'] = [
    '#type' => 'address',
    '#default_value' => \Drupal::config('system.site')->get('address') ?? [
      'country_code' => 'US',
    ],
    '#used_fields' => [
      AddressField::ADDRESS_LINE1,
      AddressField::ADDRESS_LINE2,
      AddressField::ADMINISTRATIVE_AREA,
      AddressField::LOCALITY,
      AddressField::POSTAL_CODE,
    ],
    '#available_countries' => ['US'],
  ];
 
  // Add a custom submit handler for our new values.
  $form['#submit'][] = 'MYMODULE_site_address_submit';
}
 
/**
* Custom submit handler for our address settings.
*/
function MYMODULE_site_address_submit($form, FormStateInterface $form_state) {
  \Drupal::configFactory()->getEditable('system.site')
    ->set(‘address’, $form_state->getValue('site_address'))
    ->save();
}

Lastly we should do some house cleaning in case our module gets uninstalled for any reason. In the same directory as the MYMODULE.module file, let’s add a MYMODULE.install file with the following code:

/**
 * Implements hook_uninstall().
 */
function MYMODULE_uninstall() {
  // Delete the custom address config values.
  \Drupal::configFactory()->getEditable('system.site')
    ->clear(‘address’)
    ->save();
}

That’s it! Now we have a way to provide location information to the global site configuration. Using that data, I’ll be able to display this information elsewhere as text or as a Google Map. Being able to use the same features that Address field types have, I can leverage other modules that display address information or build my own displays, because I now have reliably structured data to work with.


About the Author

James was introduced to Drupal in college and has been building Drupal sites ever since. He's coming to Aten with 8 years of experience building websites of all shapes and sizes. With strong focus on front-end and responsive build processes, James is constantly exploring new web technologies. Outside of work, James is an avid cook and home-brewer. When he's not at home trying new recipes, he's...

Read More