Sulu Headless exposing form config
In one of our recent projects, we are using Sulu CMS with the Sulu Headless bundle and a React frontend. Additionally, we are also using the Sulu Forms bundle for form handling.
The Sulu Headless bundle out-of-the-box supports a lot of different content types. However, Sulu Forms are not yet supported by the Sulu Headless bundle. Luckily, the Sulu bundles in general are open for extension and this is what we did.
All we had to do is to implement an instance of the ContentTypeResolverInterface
and tag that service with the
sulu_headless.content_type_resolver
tag. This is how a simple approach for return form data looks like:
use Sulu\Bundle\FormBundle\Form\BuilderInterface;
use Sulu\Bundle\HeadlessBundle\Content\ContentTypeResolver\ContentTypeResolverInterface;
use Sulu\Bundle\HeadlessBundle\Content\ContentView;
use Sulu\Component\Content\Compat\PropertyInterface;
use Sulu\Component\Content\Compat\Structure\PageBridge;
use Sulu\Component\Content\Compat\Structure\StructureBridge;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Exception\MissingOptionsException;
class FormSelectionResolver implements ContentTypeResolverInterface
{
/**
* @var BuilderInterface
*/
private BuilderInterface $formBuilder;
public function __construct(BuilderInterface $formBuilder)
{
$this->formBuilder = $formBuilder;
}
public static function getContentType(): string
{
return 'single_form_selection';
}
/**
* @inheritDoc
*/
public function resolve($data, PropertyInterface $property, string $locale, array $attributes = []): ContentView
{
$return = new ContentView([], ['default' => []]);
$id = (int) $property->getValue();
if (!$id) {
return $return;
}
if (!isset($property->getParams()['resourceKey'])) {
throw new MissingOptionsException('SuluFormBundle: The parameter "resourceKey" is missing!', []);
}
/** @var string $resourceKey */
$resourceKey = $property->getParams()['resourceKey']->getValue();
/** @var PageBridge $structure */
$structure = $property->getStructure();
$form = $this->formBuilder->build(
$id,
$resourceKey,
(string) $structure->getUuid(),
$structure->getLanguageCode(),
$property->getName()
);
if (!$form) {
$form = $this->loadShadowForm($property, $id, $resourceKey);
if (!$form) {
return $return;
}
}
$formData = [];
foreach ($form->all() as $child) {
$propertyPath = $child->getConfig()->getName();
if ($child->getConfig()->getOption('property_path') !== null) {
$propertyPath = $child->getConfig()->getOption('property_path');
// get rid of "data[]" from property_path
$propertyPath = substr($propertyPath, strpos($propertyPath, '[') + 1, -1);
}
$formData[$child->getConfig()->getName()] = [
'label' => $child->getConfig()->getOption('label'),
'attr' => $child->getConfig()->getOption('attr'),
'required' => $child->getConfig()->getOption('required'),
'property_path' => $propertyPath,
'data' => $child->getConfig()->getData()
];
}
return new ContentView([], [$form->getName() => $formData]);
}
private function loadShadowForm(PropertyInterface $property, int $id, string $type): ?FormInterface
{
$structure = $property->getStructure();
if (!$structure instanceof StructureBridge) {
return null;
}
if (!$structure->getIsShadow()) {
return null;
}
return $this->formBuilder->build(
$id,
$type,
(string) $structure->getUuid(),
$structure->getShadowBaseLanguage(),
$property->getName()
);
}
}
In the resolve()
method of the ContentTypeResolverInterface
we create a form instance based on the available
parameters and in case that is not possible, build the form for the shadow language.
Once the form is built, we iterate over the form children and extract their names and parts of their configuration. For now, this gives us enough information to render the fields and send the expected form contents back to the server.