By: Raman
April 13 2018

How to use CTool features which have not been moved to Drupal core

 “Chaos Tool Suite (CTools) is primarily a set of APIs and tools to improve the developer experience.” - Drupal.org

Chaos Tool Suite (CTools) is a contributed project in Drupal that is oriented towards developers. It is essentially a set of helper classes that can be extended to simplify certain workflows and defining custom functionalities. Many contributed modules including Pathauto, Panels and Page Manager use the API provided by CTools.

Since Drupal 8, a major chunk of CTools has been moved into core. This includes TempStore, Configuration Management Initiative (CMI), Config Entities, Plugins, AJAX and Modals, and other miscellaneous helper classes. Kris Vanderwater, one of the maintainers for Chaos Tool, described CTools as “a testing ground for what Drupal core should be doing”. 

The project is still undergoing changes and development of tools that may be moved to core in the future. Let us have a look at the features of CTools that are currently available in the 8.x-3.x version.

Wizard API

Multi-step forms are usually used when we need to take a number of inputs without overwhelming the user. They can also be used to build dynamic form inputs i.e. when the form inputs of one step can be changed on the information provided in the previous step. They also allow better organization of UI and code. 

CTools provides two abstract classes – FormWizardBase and EntityFormWizardBase, which can be extended to easily make multi-step forms or wizards. The former one can be used to create custom one-off forms while the latter one to wizardify the add or edit form for Config Entities. 

The individual steps of the wizard are nothing but forms. Just like every other form in Drupal, they are classes that inherit from the FormBase class. Hence, we can control the individual steps just by implementing the abstract methods – buildForm, validateForm and submitForm in the individual steps or forms.

The Wizard API ensures that the wizard temporary value of the form_state object is always available. In case of the EntityFormWizardBase, it will contain a key named same as the entity id.

Creating a multi-step form using Wizard API

To create a multi-step form using Wizard API, we need to create a custom module, and then add forms, one form for each step. In this example, we will create a wizard for a custom config entity by extending EntityFormWizardBase. Drupal Console can be used to generate the boilerplate code to save time and effort.

  1. Navigate to the root directory of your Drupal site

    $ cd /var/www/html/drupal8
     
  2. Generate module boilerplate using Drupal Console (or create the .info.yml and the .module files manually) and answer a series of questions generated by the module generator.

    $ drupal generate:module
     Generating boilerplate code for the custom module
    Generating boilerplate code for the custom module
  3. Now, generate boilerplate code for custom config entity and answer a series of questions generated by the entity generator.

    $ drupal generate:entity:config
     
    Generating boilerplate code for custom config entity
    Generating boilerplate code for custom config entity
  4. Now create forms to add and edit our custom config entity by extending the FormBase class and implementing getFormId, buildForm and submitForm methods. Optionally, implement the validateForm method to define custom validation. Create one form for each step.

    This is where the actual magic happens. The wizard temporary value of the forms _state object is used to temporarily store the intermediate values. In case of custom one-off forms, do not forget to save the field values in the TempStore using the setTemporaryValue method. 
     public function getFormId() {
        return 'wizard_test_config_entity_first_form';
      }
    
      public function buildForm(array $form, FormStateInterface $form_state) {
        $config_entity = $form_state->getTemporaryValue('wizard')['custom_config_entity'];
        $form['first_value'] = [
          '#title' => $this->t('First'),
          '#description' => $this->t('Enter first value here'),
          '#type' => 'textfield',
          '#default_value' => $config_entity->getFirst() ?: '',
        ];
        return $form;
      }
    
      public function submitForm(array &$form, FormStateInterface $form_state) {
        $config_entity = $form_state->getTemporaryValue('wizard')['custom_config_entity'];
        $config_entity->set('first_value', $form_state->getValue('first_value'));
      }
  5. Create the wizard class by extending the EntityFormWizardBase class and implement the following methods.
      public function getWizardLabel() {
        return $this->t('Custom config entity wizard');
      }
    
      public function getMachineLabel() {
        return $this->t('custom_config_entity_wizard');
      }
    
      public function getEntityType() {
        return 'wizard_test_config_entity';
      }
    
      public function exists() {
        return '\Drupal\wizard_test\Entity\CustomConfigEntity::load';
      }
    
      public function getOperations($cached_values) {
        return [
          'label' => [
            'form' => 'Drupal\wizard_test\Form\CustomConfigEntityLabelForm',
            'title' => $this->t('Label and ID'),
          ],
          'first' => [
            'form' => 'Drupal\wizard_test\Form\FirstForm',
            'title' => $this->t('Step one'),
          ],
          'second' => [
            'form' => 'Drupal\wizard_test\Form\SecondForm',
            'title' => $this->t('Step two'),
          ],
        ];
      }
  6. Add the wizard annotation in your custom entity class, CustomConfigEntity in this example.
     "wizard" = {
        "add" = "Drupal\wizard_test\Wizard\AddEntityWizard",
        "edit" = "Drupal\wizard_test\Wizard\EditEntityWizard"
      }
    
      links = {
        "canonical" = "/admin/structure/custom_config_entity/{custom_config_entity}",
        "edit-form" = "/admin/structure/custom_config_entity/{machine_name}/{step}",
        "delete-form" =   "/admin/structure/custom_config_entity/{custom_config_entity}/delete",
        "collection" = "/admin/structure/custom_config_entity"
      }
  7. Define the routes for the wizard(s). Remember to declare _entity_wizard and tempstore_id under defaults.
    entity.custom_config_entity.add_form:
      path: '/admin/structure/custom_config_entity/add'
      defaults:
        _entity_wizard: 'custom_config_entity.add'
        _title: 'Add Custom config entity'
        tempstore_id: 'custom_config_entity.config_entity'
      options:
        _admin_route: TRUE
    
    entity.custom_config_entity.add_step_form:
      path: '/admin/structure/custom_config_entity/add/{machine_name}/{step}'
      defaults:
        _entity_wizard: 'custom_config_entity.add'
        _title: 'Add Custom config entity'
        tempstore_id: 'custom_config_entity.config_entity'
      options:
        _admin_route: TRUE
    
    entity.custom_config_entity.edit_form:
      path: '/admin/structure/custom_config_entity/{machine_name}/{step}'
      defaults:
        _entity_wizard: 'custom_config_entity.edit'
        _title: 'Edit Custom config entity'
        tempstore_id: 'custom_config_entity.config_entity'
      options:
        _admin_route: TRUE
  8. Enable the newly created module.
    $ drush en wizard_test -y
     or
    $ drupal module:install wizard_test

Now visit the route of your wizard to be greeted by the custom multi-step form.

Example of multi-step form for a custom Config Entity
 Example of multi-step form for a custom Config Entity
Example of multi-step form for a custom Config Entity
 Example of a wizard for custom one-off forms

Entity Derivatives

In Drupal 8, Plugin Derivatives can be used to iterate over annotation objects. This allows us to use a foreach loop to iterate over all the entities including blocks, comments, nodes, taxonomies, etc. 

CTools provides a helper class EntityDeriverBase which implements this functionality. This can be particularly helpful when going Headless with Drupal. We can create a plugin, use it along with the services API and perform CRUD operations on entities.

Implement the getDerivativeDefinitions() method to return all the entity derivatives and add the reference of the deriver class in the service definition annotation. Refer this repository for implementation details.

Retrieving an article bundle in JSON format using custom services
Retrieving an article bundle in JSON format using custom services

CTool Blocks and Views modules

The project also includes a couple of modules which provides improvements to core blocks and views. Note that these are marked as experimental. Before using them, enable these modules using any of the following methods:

Using UI

Navigate to Manage → Extend, select the Chaos tools blocks and views modules and click on “Install”.

Enabling Chaos tools blocks and views through admin UI
Enabling Chaos tools blocks and views through admin UI

Using terminal

$ drush en ctools_block ctools_views -y
or
$ drupal module:install ctools_block ctools_views

Now, to add the blocks provided by the Chaos tools blocks module, follow the below steps:

  1. Navigate to Manage → Structure → Block Layout and click on “Place Block” against the region you wish to add the block in.
     
  2. Select the Entity View block provided by the Chaos tools module.
     
    Blocks provided by the CTools Blocks module
    Blocks provided by the Chaos Tools Blocks module
  3. Give an appropriate Block title, select view mode and select the visibility conditions just like any other block. Click on “Save Block” to apply changes.
     
     Example of Chaos Tool Entity View blocks
    Example of Chaos Tool Entity View blocks
    The CTools project comes with many useful tools for developers to simplify workflows and promote code reusability. You may also look at the source code of other modules including Page Manager, Panels, and Pathauto to better understand the use case of features provided by Chaos Tool Suite. 

    More of these features and tools are expected to make their way into Drupal core in future. A good way to keep track of all the changes in the codebase is to follow the git repository of this branch.

Let us know in the comments below how we can help you or drop a mail at [email protected].

Raman is an Open Source enthusiast and likes to play around with web and mobile development technologies. In his free time, he loves to catch up with the episodes of Silicon Valley.