8: Compound (bundled) fields - your new best friend

We've all been there, a node form with a number of related fields, such as name or address, again and again... and again. What if you could package those fields into a widget that can be reused just by adding it to a content type. Not only can it be easily be done, but I'm going to show you how.

This tutorial will differ from most other custom field type tutorials, because we won't just be creating a field type, but the field type will be a widget with sub-fields.

Let's first take a cursory look at why you might want to use this type of field type widget, using 'name' as an example, as we will in the tutorial. When working with multiple fields, you typically have two options:

  1. Add discreet fields to the content type, such as 'First name', 'Surname' and so on
  2. Create an entity and field it, then add an entity reference to the content type

Both of these methods will work, but each has at least one drawback. With option 1, you would need to do the same thing with every content type using the same fields. With option 2, adding the entity reference doesn't necessarily provide you the rendering of its fields, nor CRUD (Create/Read/Update/Delete) services.

Fortunately, there's a third option: a custom field type that creates a bundled (compound) field. Let's create one, and I'll leave it to you to decide if it's useful to you!

The Concept

What is a compound/bundled field? It is a field that contains sub-fields. You are probably familiar with this concept if you have ever looked inside the table that contains a node's body field:



+--------------+------------------+------+-----+---------+-------+ 
| Field        | Type             | Null | Key | Default | Extra | 
+--------------+------------------+------+-----+---------+-------+ 
| bundle       | varchar(128)     | NO   | MUL |         |       | 
| deleted      | tinyint(4)       | NO   | PRI | 0       |       | 
| entity_id    | int(10) unsigned | NO   | PRI | NULL    |       | 
| revision_id  | int(10) unsigned | NO   | MUL | NULL    |       | 
| langcode     | varchar(32)      | NO   | PRI |         |       | 
| delta        | int(10) unsigned | NO   | PRI | NULL    |       | 
| body_value   | longtext         | NO   |     | NULL    |       | 
| body_summary | longtext         | YES  |     | NULL    |       | 
| body_format  | varchar(255)     | YES  | MUL | NULL    |       | 
+--------------+------------------+------+-----+---------+-------+ 

Take a look at the last three columns: body_value, body_summary and body_format. These will be familiar to you from the node form. A summary, if provided for the body text, is stored in the body_summary sub-field. The body text is stored in body_value, and the text format selected, such as Basic HTML and Full HTML, is stored in body_format.

That type of structure, in brief, is what we're aiming to create progammatically. We'll look closely at the results in our own table, later. 

Another aspect of the concept of what we'll be doing that is worth knowing, up front: we're not actually creating a field, we're creating a field type. This, too, we'll take a closer look at, later.

So, what will we be creating? A field type widget for a person's name, the sub-fields of which will be, as follows:

  • Title
  • First name
  • Middle name/initial
  • Last name
  • Maternal last name
  • Suffix

The field type is constructed as a widget, which is a technical term for thingy, as in something tangible, but in a somewhat intangible sort of way. Yup, a thingy. The widget has the following components:

  • Module files - the files that define the module scaffolding that wraps the widget plugin
  • Field Type - defines the structure of the field type and how to determine if the field using it should be considered empty 
  • Field Widget - defines the structure of the content form field(s) that will accept values for the field, as well as any custom code needed to process the input
  • Field Formatter - formats the field data to pass to the twig file
  • Twig file - the default rendering of fields using the widget

We will create each of these in this five-part tutorial series. Let's get started!

the Module Files

This widget is made available to Drupal as a plugin wrapped in a module. Let's start by creating the files that are typical for a module.

name_field_type.info.yml


name: 'Name'
description: 'Provides a bundled name field.' 
type: module
core: 8.x
package: 'Field types'
dependencies:
  - field
  - node
  - text

The info.yml file is required for Drupal to pay attention to the rest of the module's files. The contents of this one are vanilla, other than some enumerated core dependencies.

name_field_type.module


/**
 * @file
 * Contains name_field_type.module.
 */
use Drupal\Core\Routing\RouteMatchInterface;
/**
 * Implements hook_theme().
 */
function name_field_type_theme($existing, $type, $theme, $path) {
    $theme = [
        'name_field_widget' => [
            'variables' => [
                'title' => NULL,
                'first_name' => NULL,
                'middle_name' => NULL,
                'last_name' => NULL,
                'maternal_last_name' => NULL,
                'suffix' => NULL,
            ],
        ],
    ];
    return $theme;
}
/**
 * Implements hook_help().
 */
function name_field_type_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    // Main module help for the name_field_type module.
    case 'help.page.name_field_type':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Field type widget example.') . '</p>';
      return $output;
    default:
  }
}

The .module file contains an implementation of hook_theme. This will pave the way for the variables that will be passed to the twig file, which we'll create later.

That's it for the module files. At this point we could do

drush en name_field_type

and enable the module, though of course the module won't do anything yet. In Part 2 we move on to the file that defines the widget field type.

Part 2


Of course, the Drupal Community isn't the only place to give back. It's a complex world, and there are as many worthy causes as their are melodies, with most being served by multiple charitable organizations. Here are a few of those that strike a 7th chord with me.