Skip to content

Concepts

Three pieces work together: a kit, the kits service that creates kits, and a FormFactory that collects kits and returns the final $form array.

Kit

A kit is a small object that describes one render element. Every kit in this module extends Drupal\formfactorykits\Kits\FormFactoryKit (src/Kits/FormFactoryKit.php), which extends Drupal\kits\Kit from the Kits module.

A kit keeps three pieces of state:

State What it holds Ends up in the render array?
parameters Form API properties without the #, for example title, type, required Yes, each one becomes #<name>
context Data that is not a Form API property: the kit's ID, its parents, helper settings Only indirectly (parents becomes #parents)
children Other kits appended with append() Yes, keyed by each child's ID

Each kit class declares the Form API element type it produces in a static $type property (for example TextFieldKit::$type = 'textfield') and a default ID in a static $id property. The constructor copies $type into the type parameter, so a new kit already produces #type.

Setters are fluent: they return the kit (static), so calls chain.

$name = $kits->textField('name')   // ID "name", #type "textfield"
  ->setTitle('Name')               // #title
  ->setDescription('Your name')    // #description
  ->setRequired();                 // #required => TRUE

The kits service

formfactorykits (Drupal\formfactorykits\Services\FormFactoryKitsService) has one factory method per kit, for example textField(), details() and submit(). Every factory method has the same signature:

public function textField(?string $id = NULL, array $parameters = [], array $context = []): TextFieldKit;
  • $id is the array key the element gets in the form. When it is NULL, the kit class's default ID is used (textfield for TextFieldKit).
  • $parameters pre-sets Form API properties, without the #. Anything you pass here goes straight into the render array.
  • $context pre-sets non-render data.

The full list of factory methods is in the Kit reference section of this site.

FormFactory

formfactory (Drupal\formfactory\Services\FormFactoryService, from the Form Factory module) wraps an existing $form array:

Method Purpose
load(array $form, ?FormStateInterface $form_state = NULL) Start from an existing $form array (it can be empty)
append(KitInterface $kit) Add a top-level kit
getForm(): array Return the original $form with every appended kit's array added
getFormState() Return the form state passed to load()
setTree(bool $isTree = TRUE) Set #tree on the form
attach(string $item, string $type = 'library') Add an #attached entry
setDrupalSetting(string $namespace, string $key, string $value) Add a drupalSettings entry

How a kit becomes a render array

FormFactoryKit::getArray() builds a new array each time it is called:

  1. If the kit has parents in its context, it writes #parents.
  2. Every parameter whose value is not NULL is written as #<name>.
  3. The children's arrays are added, keyed by child ID (Kit::getChildrenArray()).

FormFactoryService::getForm() starts from a copy of the loaded $form and sets $form[$kit->getID()] = $kit->getArray() for every appended kit. Kits that group their children (vertical tabs) also add their children at the top level, next to the group element.

$form_factory = \Drupal::service('formfactory')->load([]);
$form_factory->append(
  \Drupal::service('formfactorykits')->textField('name')
    ->setTitle('Name')
    ->setRequired()
);
$form = $form_factory->getForm();

produces

[
  'name' => [
    '#type' => 'textfield',
    '#title' => 'Name',
    '#required' => TRUE,
  ],
]

The output contains no objects except the values you put in (for example a TranslatableMarkup title). Kits are not kept in the form, so the form cache, AJAX rebuilds and hook_form_alter() see a normal array.

Building twice from the same kits

When a parent kit builds its children, it appends its own ID to each child's parents context (Drupal\kits\Kit::getChildrenArray()). That is a side effect on the child kit. Calling getForm() or getArray() twice on the same kit tree therefore repeats the parent ID in #parents (['outer', 'outer']). Build each kit tree once.

Nested #parents

Children of most container kits (for example DetailsKit) get #parents set to their ancestors' IDs only and #group set to their direct parent's ID. For a text field a inside a details element outer, the output is:

'outer' => [
  '#type' => 'details',
  'a' => [
    '#parents' => ['outer'],
    '#type' => 'textfield',
    '#group' => 'outer',
  ],
],

Form API's own default for #parents (when #tree is on) is the ancestors plus the element's own key (['outer', 'a']). Check the submitted values of nested fields in your form. ContainerKit does not pass its ID down as a parent, and the children of tabs (TabKit) do not get #parents at all. See the Nesting guide for examples.