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;
$idis the array key the element gets in the form. When it isNULL, the kit class's default ID is used (textfieldforTextFieldKit).$parameterspre-sets Form API properties, without the#. Anything you pass here goes straight into the render array.$contextpre-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:
- If the kit has parents in its context, it writes
#parents. - Every parameter whose value is not
NULLis written as#<name>. - 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.