Skip to content

Writing your own kit

The kits in this module cover Form API basics. A custom kit packages an element you build often (a rating widget, a site-specific address block) so that forms can create it in one line.

1. Extend FormFactoryKit (or FieldKit)

FormFactoryKitInterface extends Drupal\kits\KitInterface, which has 15 methods. Rather than implementing them yourself, extend one of the abstract base classes, which implement all of them:

  • Drupal\formfactorykits\Kits\FormFactoryKit for any element;
  • Drupal\formfactorykits\Kits\Field\FieldKit for an input. It adds setValue(), setDefaultValue(), setPrefix() and setSuffix().

Set two static properties:

  • $id: the default array key when no ID is passed;
  • $type: the Form API #type the kit produces.

2. Compose traits

Add use statements for the setters you want (see the trait reference). Traits that read a key constant from your class (SizeTrait, PatternTrait, MultipleTrait, ThemeTrait) need that constant defined.

3. Add your own setters

A setter calls $this->set('<property>', $value) and returns static. The property name is the Form API key without #. For anything that is not a direct property, override getArray(), call parent::getArray() and adjust the result.

Example: a 1–5 rating

<?php

namespace Drupal\mymodule\Kits;

use Drupal\formfactorykits\Kits\Field\FieldKit;
use Drupal\formfactorykits\Kits\Traits\DescriptionTrait;
use Drupal\formfactorykits\Kits\Traits\RequiredTrait;
use Drupal\formfactorykits\Kits\Traits\TitleTrait;

class RatingKit extends FieldKit {
  use DescriptionTrait;
  use RequiredTrait;
  use TitleTrait;

  public static ?string $id = 'rating';
  public static ?string $type = 'radios';
  public static ?string $title = 'Rating';

  private int $max = 5;

  public function setMax(int $max): static {
    $this->max = $max;
    return $this;
  }

  public function getArray(): array {
    $artifact = parent::getArray();
    $range = range(1, $this->max);
    $artifact['#options'] = array_combine($range, $range);
    $artifact['#attributes']['class'][] = 'mymodule-rating';
    return $artifact;
  }

}

Because $title is set, the constructor gives the kit a translated default title (t('Rating')) unless one is passed in.

4. Create the kit

Every kit has a static create() method that takes the kits service, so the simplest way to use a custom kit is to call it directly:

use Drupal\mymodule\Kits\RatingKit;

$kits = \Drupal::service('formfactorykits');
$form_factory->append(
  RatingKit::create($kits, 'quality')
    ->setTitle('Quality')
    ->setMax(3)
    ->setRequired()
);

This generates:

'quality' => [
  '#title' => 'Quality',
  '#type' => 'radios',
  '#required' => TRUE,
  '#options' => [1 => 1, 2 => 2, 3 => 3],
  '#attributes' => ['class' => ['mymodule-rating']],
],

5. Optional: add a factory method

To call $kits->rating() like the built-in kits, extend the service class and register your subclass as a service.

<?php

namespace Drupal\mymodule\Services;

use Drupal\formfactorykits\Services\FormFactoryKitsService;
use Drupal\mymodule\Kits\RatingKit;

class MyKitsService extends FormFactoryKitsService {

  public function rating(?string $id = NULL, array $parameters = [], array $context = []): RatingKit {
    return RatingKit::create($this, $id, $parameters, $context);
  }

}
# mymodule.services.yml
services:
  mymodule.kits:
    class: Drupal\mymodule\Services\MyKitsService

FormObjectBase always injects the formfactorykits service. If you want your methods available there too, replace that service's class from a service provider (mymodule/src/MymoduleServiceProvider.php, a class extending Drupal\Core\DependencyInjection\ServiceProviderBase whose alter() calls $container->getDefinition('formfactorykits')->setClass(MyKitsService::class)). Only one module on a site can do this, so prefer a separate service in shared modules.

Implementing the interface directly

If you cannot extend FormFactoryKit, implement FormFactoryKitInterface yourself. FormFactoryService::getForm() calls isChildrenGrouped(), getID() and getArray() on every appended kit, and getChildrenArray() on grouped ones. isChildrenGrouped() is not part of the interface but is required in practice, because getForm() calls it on every kit.