Apr 17 2019
Apr 17

One of the challenges front-end developers face is adding new components to entity templates that exist outside of what is defined in the Field API; or in other words, adding dynamic components that aren’t really fields. Often this can be easily done by throwing the custom markup in a .html.twig file and calling it a day. But if you’re working on something that needs to be reusable, or if you’re collaborating with a site builder who doesn’t write code, the custom template route can be limiting.

Enter hook_entity_extra_field_info().

Content Moderation: A “Pseudo-Field” in Core

Drupal’s documentation says this hook “exposes ‘pseudo-field’ components on content entities.” You can see this hook in action with the Content Moderation module in core. All moderation-enabled entities can have an option box, placed via that entity’s Manage Display page, that contains a widget to update an entity’s moderation state in place rather than clicking through to the edit page.

Drupal's extra fields interface

The moderation option isn’t a real field. Rather, it’s what Drupal calls a “Pseudo Field.” But by using hook_entity_extra_field_info(), you wouldn’t know the difference. The moderation option can be moved around and configured for various display modes, just like “real” fields.

Using hook_entity_extra_field_info in a Custom Module

On a recent project, we needed to integrate a newer commenting service called Coral Talk. After searching, we learned that no module existed to integrate this service in Drupal. This presented a perfect use case for an Extra Field, and only needed two hooks for the bulk of the work:

/**
 * Implements hook_entity_extra_field_info().
 */
function coral_talk_entity_extra_field_info() {
  // Load commenting configuration.
  $config = \Drupal::config(coral_talk.settings');
  $extra = [];
 
  // Loop over the content types configured to have comments
  // and get their bundle name.
  foreach ($config->get('content_types') as $bundle) {
    if ($bundle) {
      // Add info for Extra Field to nodes only, specific to configured
      // content types. This determines what shows on Manage Display.
      $extra['node'][$bundle]['display'][‘coral_talk_comments'] = [
        'label' => t(‘Coral Talk Comments'),
        'description' => t('Place commenting on the page.'),
        'weight' => 100,
        'visible' => TRUE,
      ];
    }
  }
 
  // Return our new extra field.
  return $extra;
}

After a cache clear, this new field will appear on the configured content types’ Manage Display page and can be placed on the content type along with the other fields for that content type. Now that the field is defined, it needs some info for what should be rendered to the page. This is handled by Drupal’s hook_ENTITY_TYPE_view() hook.

/**
 * Implements hook_ENTITY_TYPE_view().
 */
function coral_talk_node_view(
  array &$build,
  \Drupal\Core\Entity\EntityInterface $entity,
  \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display,
  $view_mode
) {
  // 1. Check to see if our new field should be rendered on the entity display.
  // 2. Determine whether the user has permission to add comments.
  $condition = (
    $display->getComponent(‘coral_talk_comments') &&
    \Drupal::currentUser()->hasPermission('create coral comment')
  );
 
  if ($condition) {
    $config = \Drupal::config(coral_talk.settings');
 
    // Add the new field to the $build array with a call to a custom theme
    // hook to render the comments. Pass necessary config into comment
    // settings.
    $build[‘coral_talk_comments'] = [
      '#theme' => 'coral_talk_comments',
      '#domain' => $config->get('domain') ?? '',
    ];
  }
}

After another cache clear, we’ll now see our comments being rendered to our content types in whichever view mode they’re enabled on. The moves setup of comments outside of code and into a place that’s more accessible and flexible for various users.

This approach is great for simple scenarios. One drawback, however, is that it’s not possible to define any custom configuration options for these pseudo fields. Each extra field is identical, and any configuration has to be hard coded in these hooks. This presents challenges for site builders, who might want to configure comments differently per content type however. Fortunately, there is a solution in contrib that changes how Extra Fields are defined and allows for developers to add configuration to each field. In the next post, we’ll explore the Extra Field Settings Provider module.

Apr 27 2018
Apr 27

In the previous post we looked into Pantheon hosting and how we can use it to easily create a suite of similar websites without having to build them individually each time. Often the requirement isn’t only easily creating new sites, but having to maintain them easily as well. When you have dozens or hundreds of websites that need changes applied to them, managing each one individually through Pantheon’s dashboard becomes a bottleneck. Fortunately Pantheon offers a command line interface that allows developers to automate much of that maintenance. In this post we’ll take a look at using Terminus to manage our sites.

Understanding Pantheon’s Framework

Before we can start rolling out features to multiple sites, it is helpful to understand how Pantheon groups the websites it hosts. Websites can be first grouped into an Organization. Within that, they can be tagged in any manner that makes sense for your needs. Both the organization and the tags can be used to filter sites into more targeted groups.

Each site then gets three environments; dev, test, and live are their machine names. Those machine names are important, as we’ll need to know which environment we’re targeting when we do our deployments. A single site also gets a machine name, like my-awesome-site. The combination of site name and environment name create a single instance identifier, which we use in our Terminus commands. For example, to clear Drupal’s cache on a live environment we’d run:

terminus remote:drush my-awesome-site.live -- cache-rebuild

A deployment on Pantheon has to follow a specific process, whether done via the dashboard or through Terminus. First, code must be deployed to the dev environment. Normally this is done with Git by pushing new code into the master branch on Pantheon’s repo. For features we’re deploying to multiple sites, the code must be pushed to the Upstream and then pulled from there. In the dashboard, this takes the form of a button that appears to alert you to new changes. In Terminus, you’d run the following command. Note, the --updatedb flag ensures any Drupal database updates get run as well.

terminus upstream:updates:apply my-awesome-site.dev --updatedb

Second, we have to move those updates to testing and then to production. Again, the dashboard provides a button on those environments when there are updates that can be made to them. In Terminus, this is done with:

terminus env:deploy my-awesome-site.test --updatedb --cc --note=”Deployed new feature.”

As before --updatedb runs the database updates, --cc rebuilds Drupal’s cache, and --note is the description of the updates that gets added to the Pantheon dashboard.

There are many other actions you can handle with Terminus. Their documentation covers the full list. However, out of the box Terminus has the same limitation that the dashboard has. You can only run a command on one site at a time. Thankfully, Terminus has additional plugins that solve this problem for us.

New Commands with Terminus Plugins

Terminus is built on PHP and managed with Composer. This allows for new commands to be built and distributed on Pantheon’s Terminus Plugin Library. We’ll need to install two plugins to run Terminus commands on multiple sites at once: Terminus Mass Update and Terminus Mass Run. Mass Update is created by Pantheon and runs the upstream:updates:apply command on a list of sites that get piped into it. Mass Run builds on that idea, by using the same piping logic and implements it onto more commands. With it you can run Drush commands, create site backups, and deploy code among other things.

To get the list of sites, we’ll use the org:site:list command. We could also use site:list, however since Custom Upstreams are an Organization level feature we’ll more than likely want to filter by Organization; org:site:list takes the name of the organization we want to filter by. To get a list of the Organizations you have access to, run terminus org:list. This returns both the machine name and the ID number of the Organizations, either will work for org:site:list.

Running terminus org:site:list aten will return a table of all sites in Aten’s Organization account. However, we still might only want a subset of those sites. This is where tagging comes in. Adding the --tag flag to our command lets us get only sites we’ve tagged with whatever is passed in. To see all sites tagged with “US” our command becomes terminus org:site:list aten --tag=US. This gets us closer, however it still returns a table of all site information. We only need the site ID numbers as a list for our Mass Run and Mass Update commands. To get this list we’ll add --format=list to our command, making the entire thing:

terminus org:site:list aten --tag=US --format=list

Now that we have a list of the site IDs we want to update, all we need to do is pipe that list into our plugin commands. To deploy a new feature from our upstream, we’d run:

terminus org:site:list aten --tag=US --format=list | terminus site:mass-update:apply --updatedb

Moving that feature through Pantheon’s environments is:

terminus org:site:list aten --tag=US --format=list | terminus env:mass:deploy --sync-content --cc --updatedb --env=test --note="Updated Drupal Core."

Removing a user from all sites they exist on becomes:

terminus org:site:list aten --tag=US --format=list | terminus remote:mass:drush --env=live -- ucan bad-user

Long Commands, Amazing Results

At this point you’ve probably noticed the commands we’re using have become very verbose. This is one downside of this approach: the commands themselves are not intuitive at first glance. For common tasks creating aliases can help simplify this. Leveraging the terminal’s history to bring up past commands and then modifying them speeds up more one-off tasks. But the ability to manage our websites en masse becomes a huge time saver over clicking our way through the dashboard for dozens of sites.

Apr 18 2018
Apr 18

Over the last couple years, organizations have been coming to us with a new problem. Instead of needing a single website, they need dozens, if not hundreds. They might be large universities with many departments, an association with independent franchises nationwide, or a real estate developer with offerings all over the world. Often, these organizations already have several websites supported by different vendors and technologies. They’ve become frustrated with the overhead of maintaining Drupal sites built by one vendor and Wordpress sites by another. Not to mention the cost of building new websites with a consistent look and feel.

While the details may vary, the broad ask is the same. How can we consolidate various websites onto a single platform that can be spun up quickly (preferably without developer involvement) and update and maintain these en masse, while maintaining enough structure for consistency and flexibility for customization. Essentially, they want to have their cake and would also like to eat it.

Over this series of posts, we’ll break down the various parts of this solution. We’ll first look at Pantheon’s hosting solution, and how its infrastructure is set up perfectly to give clients the autonomy they want. Then we’ll look at the command line tools that exist for developers to easily manage updates to dozens (if not hundreds) of websites. Lastly, we’ll look at the websites themselves and how Drupal 8 was leveraged to provide flexible website instances with structured limits.

Pantheon and Upstreams

Pantheon is a hosting solution designed specifically for Drupal and Wordpress websites. For individual sites they offer a lot of features, however the ones we’re most interested in are single click installations of a new website and single click updates to the code base. Using a feature called Upstreams, users can create fresh installs of Drupal 7, Drupal 8, or Wordpress that all reference a canonical codebase. When new code is pushed to any of those Upstreams, any site installed from it gets notified of the new code, which can be pulled into the instance with the click of a button.

Outside of the default options Pantheon maintains internally, developers can also build their own Custom Upstreams for website creation. Anyone with access to the Upstream can log into Pantheon and click a button to install a new website based on that codebase. In short this codebase will handle installing all of the features every website should have, establish any default content necessary, and be used to roll out new features to the entire platform. This setup allows non-technical users to easily create new websites for their various properties, and then handoff specific websites to their appropriate property managers for editing. We’ll go over more specifics of this codebase in a later post.

Since a developer is no longer required for the creation of individual sites, this frees up a lot of time (and budget) for building new features or keeping on top of maintenance. The process for rolling out updates is simple: the developer writes code for a new feature and pushes it to the upstream repository. Once pushed, every site connected to this upstream will get an alert about new features and a shiny button that pulls them in with a single click.

Pantheon and Organizations

At this point it’s worth mentioning that custom upstreams are a feature of a special account type called an Organization. An organization is used to group multiple websites, users, and Custom Upstreams under one umbrella. Organizations also come with additional features like free HTTPS and code monitoring services. It’s recommended that each organization signup with their own organization account, rather than use one tied to their development partner. This gives them full control over who can create new websites using their Custom Upstream, who can manage all their websites, and who can only access specific websites.

Organization accounts and Custom Upstreams go a long way in helping organizations reduce the overhead they may have from managing several properties simultaneously. Having the option to create an infinite number of websites in-house helps reduce the cost of growth. Having every website using the same codebase means new features can easily be rolled out to the entire platform and security vulnerabilities can be handled quickly.

The only downside with this approach is updates are generally applied one site at a time. The developer can push the code to the Custom Upstream, but it’s necessary to log into every website and click the button to update that site. For a handful of sites, this might be manageable. For dozens to hundreds, this problem becomes tedious. In the next post we’ll look at some of the scripted solutions Pantheon has for applying and managing an ever growing number of websites at once.

Jan 23 2018
Jan 23

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: [email protected]
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.

Jan 23 2018
Jan 23

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: [email protected]
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.

Apr 19 2017
Apr 19

Custom styled form elements are a common thing to see in a design. That’s because default form styles vary visually from browser to browser and OS to OS. It makes sense that we’d want these elements styled consistently. Styling them is pretty straightforward, with the exception of select dropdowns which can be more complex. Recently, I ran into an unexpected problem when working on a site that needed a branded admin experience.

Styling Radio and Checkbox Buttons

There’s a simple method of styling radio buttons and checkboxes I’ve been using for a while. I first saw it from the people at Tuts+, and they provided this Pen demoing the technique. Briefly explained, we visually hide the input for our radios/checkboxes and draw a new one using :before and :after pseudo elements on the label element. CSS’ :checked selector allows us to toggle our styles based on if the input is checked or not. This technique relies on appropriately marked up inputs and labels, for example:

<div class=”form-element”>
  <input type=”checkbox” id=”click-me”>
  <label for=”click-me”>Click Me</label>
</div>

Clicking the label (containing the fake checkbox styling) will toggle the state of the real checkbox that’s visually hidden.

Drupal’s Admin Interface

One thing I learned while working with some of Drupal’s admin interfaces is that they only supply the input, and not an accompanying label. This seemed especially true in tabled interfaces, where you’d check off rows of content and perform some action on the selected items. Since we’re hiding an input that doesn’t have a label to attach the visuals to, we just end up with a blank space. There were several options we had for how to address this issue.

1. Drop the Custom Styles

The simplest is to just rely on browser defaults for checkboxes and radios. It’s not a great option, but it is an affordable one for tight budgets.

2. Create the Missing Labels

This ended up being my first approach to fixing this, and became more akin to a game of Whack-a-Mole than I anticipated. After going through various preprocess functions, alters, and render functions I was still encountering inputs that were missing labels. Some I was never able to fully track down where the markup was coming from. Manually finding and fixing every missing label might be a viable solution if your website or application has only a handful of places you need to update. However this is not the most scalable solution, and if your product grows this can quickly become a financial black hole.

3. Create the Missing Labels… with Javascript

Instead of trying to find every place that creates a checkbox or radio on the server side, we could use Javascript to target every checkbox or radio input that is not followed by a label. From there, we just create the label element and insert it after the selected inputs. This is how that might look using jQuery, though it can also be done with Vanilla JS.

This is great, as it solves the problem for every input in one fell swoop. One downside here is the Javascript dependency. Should your Javascript not run for any reason, you’re still left with the original problem of missing inputs. Another is page rendering. User’s might be left with a janky experience as Javascript inserts these elements into the DOM.

4. Drop the Custom Styles… for Older Browsers

In the end, this was the solution that won out. Using CSS Feature Queries and CSS’ appearance property, we’re able to provide styled inputs for most modern browsers and then fall back to default styles in browsers that lack the support we need. This gives us our custom styles, without the scaling problem of #2, and the Javascript dependency of #3. The downside to this solution is that all versions of Internet Explorer and Firefox will use their browser defaults.

Firefox was a surprise to me, as the documentation says it supports appearance. However in practice what I got was a less appealing version of the browser default styles. Also surprisingly was by checking for only -webkit-appearance support, Edge still gets our custom styles applied. This all sat well with me for a working solution. Every team and project has it’s own constraints, so your mileage may vary.

Nov 15 2016
Nov 15

Recently, I was creating a form that provided a list of options as checkboxes and needed to include helper text for each individual checkbox. While the Form API in Drupal 7 has a #description attribute, for checkboxes and radios it applies that as text for the entire group. After a lot of looking, there didn't seem to be a way that allowed for passing descriptions into each item in the #options array that is expected. I discovered the FAPI's #after_build callbacks, which turned out to be just the solution I needed.

Defining an #after_build callback

The FAPI documentation describes #after_build pretty well: “An array of function names which will be called after the form or element is built.” This allows us to make further alterations to our form elements after they're processed by Drupal, but before they're rendered to HTML. In the case of checkboxes and radios, the FAPI elements for each individual checkbox/radio are inserted by Drupal during the initial form build. That means in order to alter those specific elements, we'll need to access the form array after the initial build. Hence, #after_build.

To get started, add some #after_build callbacks to your element. This can be done either through a new form element definition (as shown below), or by adding new elements to #after_build through hook_form_alter().

$form['form_checkboxes'] = array(
  '#type' => 'checkboxes',
  '#title' => t('My Checkboxes'),
  '#options' => array(
    'option_1' => 'Option 1',
    'option_2' => 'Option 2',
  ),
  '#options_descriptions' => array(
    'option_1' => 'Description 1',
    'option_2' => 'Description 2',
  ),
  '#default_value' => variable_get('form_checkboxes', array()),
  '#after_build' => array('_option_descriptions'),
);

This will call a function named _option_descriptions, and pass it arguments for the $element and the $form_state that we can make alterations on. Some of you might have noticed the #options_descriptions key, which isn't in the FAPI documentation. We'll get to that shortly, but first let's look at our callback function.

Custom #after_build callback with #options_descriptions

All functions defined in #after_build are expected to return the $element argument after we've made our changes. As mentioned earlier, #options_descriptions isn't a standard FAPI option. Instead, it's a custom convention we'll use that our callback function will expect. Each option in the #options array shares a key with the items in the #options_descriptions array. That shared key is the glue between each option and its description. Writing our custom callback to take advantage of this, we end up with:

/**
 * Provide a description to each option in the element
 */
function _option_descriptions($element, &$form_state) {
  foreach (element_children($element) as $key) {
    $element[$key]['#description'] = t('!description', array(
      '!description' => $element['#options_descriptions'][$key],
    ));
  }
 
  return $element;
}

While we only needed a couple lines of code, let's look into what they do. First we're looping over the results of our $element passed into Drupal's element_children() function. This looks over our $element array for any children, or any key that doesn't start with # (signifying that it's renderable). In the case of our checkboxes, this becomes each individual checkbox. This function returns their keys as an array that we can then loop over.

Next we alter the render array for each checkbox, adding a #description to it and assigning it the description with the shared key in our #options_descriptions array. Our final checkboxes now look like this:

Radio buttons with individual descriptions

Since each input is a renderable item, and we're leveraging Drupal's #description element for them, everything gets passed through the same theme hooks you'd expect for Drupal forms. These descriptions can get templated, preprocessed, or altered as you would anything else.

Success!

This example focused on checkboxes, however, this code works the same for groups of radio buttons as well. It's worth mentioning that this seems to be something that already works in Drupal 8, although it isn't well documented yet.

Feb 03 2015
Feb 03

Solid imagery can make or break a design. In our current world of changing screen sizes, screen resolutions, and bandwidth the effective use of imagery has not only become more important, but a lot more challenging.

You may want to serve up a scaled-down image in an effort to optimize performance. You may want a scaled-up image to optimize for high resolution devices. Or you may want to serve the same image, cropped differently to work better with layout changes at different breakpoints. HTML5’s ‘picture’ element addresses these challenges, and so many more.

Using Drupal, we can automate the creation of the needed versions of a single image, making content creation that much easier. In part 2 we’ll look at opening up control over the art direction of the images, while still maintaining the design’s layout rules.

Setting up the image stack

First we need to create the image styles for our breakpoints. We have three contexts to plan for; small, medium, and large displays. There should be an image style for each, and for now it should have a Scale and Crop effect set to our dimensions.

Image style list

Image style effects

Setting up Breakpoints

Drupal needs to be made aware of the breakpoints our site uses, and the Breakpoints module does just that. Install Breakpoints and its dependency, CTools. The Breakpoints module has two ways to define breakpoints, in the interface or in your site theme’s .info file. We’ll use the interface (admin/config/media/breakpoints) to create three breakpoints.

Breakpoints module configuration

Next you need to create a Breakpoint Group (admin/config/media/breakpoints/groups/add). Imagine you have several more breakpoints defined, but you want to pick specific ones to apply to one component of the site. With Breakpoint Groups, you have that ability.

Breakpoints module group configuration

Setting up the Picture module

While the Breakpoints module handles the definition and organization of breakpoints, the Picture module uses Breakpoint Groups to create the markup. Enable the Picture module and add a new Picture Mapping (admin/config/media/picture).

After you’ve chosen your Breakpoint Group, you’ll have some options as to what should happen at each breakpoint. In our case we want to match each breakpoint up to the appropriate image style we created earlier.

Picture module image style mapping

Applying this to an image field

Once all this has been set up, we can now use our Picture Mapping as a field formatter for an image field on the site. Visit the Manage Display tab for a content type with an image field, and set the Format to the Picture Mapping we set up.

Picture module field configuration

One important thing to notice is the Fallback Image Style. This is what image will be used in browsers that do not support media queries.

Wrapping up

If you create a piece of content, the rendered image should get switched out if you view the full node and resize the window. The only downside to this is Drupal is deciding what the focal point of the image is, which may not always be desirable. In the next part of this tutorial, we’ll look at using the Manual Crop module in order to give that control back to content editors.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web