component_library-1.0.x-dev/src/Form/ComponentLibraryVariantForm.php
src/Form/ComponentLibraryVariantForm.php
<?php
declare(strict_types=1);
namespace Drupal\component_library\Form;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\component_library\Entity\ComponentLibraryPattern;
use Drupal\component_library\Entity\ComponentLibraryVariant;
use Drupal\component_library\Event\ConvertYamlPlaceholdersEvent;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Twig\Error\SyntaxError;
/**
* Form controller for the component library variant form.
*/
final class ComponentLibraryVariantForm extends EntityForm {
protected RendererInterface $renderer;
public function __construct(RendererInterface $renderer) {
$this->renderer = $renderer;
}
public static function create(ContainerInterface $container): self {
return new self(
$container->get('renderer'),
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state): array {
$form = parent::form($form, $form_state);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $this->entity->label(),
'#description' => $this->t('Label for the component library variant.'),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $this->entity->id(),
'#maxlength' => 31,
'#machine_name' => [
'exists' => [self::class, 'exists'],
],
'#disabled' => !$this->entity->isNew(),
];
$code_mirror_settings = [
'autoCloseTags' => TRUE,
'buttons' => [
'undo',
'redo',
'enlarge',
'shrink',
],
'foldGutter' => TRUE,
'lineNumbers' => TRUE,
'lineWrapping' => TRUE,
'styleActiveLine' => TRUE,
'toolbar' => TRUE,
'lineSeparator' => "\n",
];
$form['template'] = [
'#type' => 'codemirror',
'#codemirror' => $code_mirror_settings + ['mode' => 'html_twig'],
'#title' => $this->t('Template'),
'#default_value' => $this->entity->get('template'),
'#element_validate' => [[self::class, 'twigValidate']],
];
$form['pattern'] = [
'#title' => $this->t('Pattern'),
'#type' => 'select',
'#options' => $this->getPatternOptions(),
'#require' => TRUE,
'#default_value' => $this->entity->get('pattern') ?? $this->getRequest()->get('pattern'),
'#ajax' => [
'callback' => [self::class, 'previewDataCallback'],
'event' => 'change',
],
'#disabled' => !$this->entity->isNew(),
];
$code_mirror_settings = [
'foldGutter' => TRUE,
'lineNumbers' => TRUE,
'lineWrapping' => TRUE,
'toolbar' => FALSE,
'readOnly' => TRUE,
'mode' => 'yaml',
];
$pattern_entity = $this->entity->getPatternEntity();
$form['data'] = [
'#type' => 'codemirror',
'#codemirror' => $code_mirror_settings,
'#title' => $this->t('Preview data'),
'#default_value' => $pattern_entity ? $pattern_entity->get('data') : NULL,
'#suffix' => '<output id="component-library-variant"></output>',
];
$form['preview'] = [
'#type' => 'submit',
'#value' => $this->t('Preview'),
'#submit' => [],
'#ajax' => [
'callback' => [self::class, 'previewCallback'],
'event' => 'click',
],
];
$form['status'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enabled'),
'#default_value' => $this->entity->status(),
];
$form['#attached']['library'][] = 'component_library/component_library';
return $form;
}
public static function exists(string $value, array $element, FormStateInterface $form_state): bool {
return ComponentLibraryVariant::load(self::idFormat($value, $form_state->getValue('pattern'))) !== NULL;
}
public static function idFormat(string $variation, string $pattern): string {
return \sprintf('%s__%s', $pattern, $variation);
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state): void {
parent::copyFormValuesToEntity($entity, $form, $form_state);
// Convert config into line feed endings so YAML config is well formed.
$value = \implode("\n", \preg_split('(\r\n|\r)', $form_state->getValue('template')));
$entity->set('template', $value);
// ID of variant should include pattern ID as a prefix, on first creation.
if ($entity->isNew()) {
$entity->set('id', self::idFormat($form_state->getValue('id'), $form_state->getValue('pattern')));
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state): void {
$entity = $this->getEntity();
$result = $entity->save();
$link = $entity->toLink($this->t('View'))->toRenderable();
$message_arguments = [
'@link' => $entity->toUrl('canonical')->toString(),
'%label' => $this->entity->label(),
];
$logger_arguments = $message_arguments + ['link' => $this->renderer->render($link)];
if ($result === \SAVED_NEW) {
$this->messenger()->addStatus($this->t('New component variation <a href="@link">%label</a> has been created.', $message_arguments));
$this->logger('component_library')->notice('Created new component variation %label', $logger_arguments);
}
else {
$this->messenger()->addStatus($this->t('The component variation <a href="@link">%label</a> has been updated.', $message_arguments));
$this->logger('component_library')->notice('Updated new component variation %label.', $logger_arguments);
}
$form_state->setRedirect('entity.component_library_pattern.edit_form', ['component_library_pattern' => $entity->getPatternEntity()->id()]);
}
/**
* Use element validator to make sure that Twig is valid.
*/
public static function twigValidate(array $element, FormStateInterface $form_state): void {
$template = $form_state->getValue($element['#parents']);
$encoded_context = $form_state->getValue('data');
try {
$context = Yaml::decode($encoded_context) ?? [];
}
catch (InvalidDataTypeException $execption) {
$context = [];
}
$markup = [
'#type' => 'inline_template',
'#template' => $template,
'#context' => $context,
];
try {
\Drupal::service('renderer')->render($markup);
}
catch (SyntaxError $exception) {
$form_state->setError($element, new TranslatableMarkup('Twig error: %error', ['%error' => $exception->getMessage()]));
}
}
/**
* Ajax callback triggered by pattern select list.
*/
public static function previewDataCallback(array $form, FormStateInterface $form_state): AjaxResponse {
$response = new AjaxResponse();
$pattern_id = $form_state->getValue('pattern');
if ($pattern_id !== NULL) {
$pattern = ComponentLibraryPattern::load($pattern_id);
$data = $pattern->get('data');
}
else {
$data = '';
}
return $response->addCommand(new HtmlCommand('#edit-data', $data));
}
/**
* Ajax callback triggered by click.
*/
public static function previewCallback(array $form, FormStateInterface $form_state): AjaxResponse {
$template = $form_state->getValue('template');
$preview_text = $form_state->getValue('data');
$yaml_error = '';
try {
$context = Yaml::decode($preview_text) ?? [];
}
catch (InvalidDataTypeException $e) {
$context = [];
$yaml_error = '<p>' . (new TranslatableMarkup('Error in preview YAML.'))->render() . '</p>';
}
$context = self::convertYamlPlaceholders($context);
$markup = [
'#type' => 'inline_template',
'#template' => $template,
'#context' => $context,
];
$iframe = [
'#type' => 'html_tag',
'#tag' => 'iframe',
'#attributes' => [
'src' => Url::fromRoute('component_library.variant_preview')->toString(),
'frameborder' => 1,
'scrolling' => FALSE,
'allowtransparency' => TRUE,
'allowfullscreen' => TRUE,
],
];
try {
$rendered = \Drupal::service('renderer')->render($markup);
}
catch (SyntaxError $exception) {
$rendered = new TranslatableMarkup('Twig error: %error', ['%error' => $exception->getMessage()]);
}
if ($yaml_error !== '') {
$rendered = $yaml_error;
}
\Drupal::service('tempstore.private')->get('component_library')->set('variant_preview', $rendered);
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#component-library-variant', \Drupal::service('renderer')->render($iframe)));
return $response;
}
public static function convertYamlPlaceholders(array $context): array {
$event_dispatcher = \Drupal::service('event_dispatcher');
\assert($event_dispatcher instanceof EventDispatcherInterface);
$event = new ConvertYamlPlaceholdersEvent($context);
$event_dispatcher->dispatch($event);
return $event->getContext();
}
/**
* Returns available pattern options.
*/
private function getPatternOptions(): array {
$storage = $this->entityTypeManager->getStorage('component_library_pattern');
$options = ['' => $this->t('- Select option -')];
foreach ($storage->loadMultiple() as $id => $pattern) {
$options[$id] = $pattern->label();
}
return $options;
}
}
