Skip to content
Snippets Groups Projects
Commit b10afe52 authored by Pierre Dureau's avatar Pierre Dureau
Browse files

Issue #3395958: [2.0.x] Update Render Element and Twig extension to SDC

parent d6cd8ddb
No related branches found
No related tags found
No related merge requests found
<?php
declare(strict_types = 1);
namespace Drupal\ui_patterns\Element;
use Drupal\sdc\Element\ComponentElement as SdcComponentElement;
use Drupal\sdc\Utilities;
/**
* Override Drupal\sdc\Element\ComponentElement.
*
* @RenderElement("component")
*/
class ComponentElement extends SdcComponentElement {
/**
* {@inheritdoc}
*
* Duplicated because generateComponentTemplate() is private.
*/
public function preRenderComponent(array $element): array {
$props = $element['#props'];
$props_alter_callbacks = $element['#propsAlter'];
// This callback can be used to prepare the context. For instance to replace
// tokens in the props.
$props = array_reduce(
$props_alter_callbacks,
fn(array $carry, callable $callback) => $this->doTrustedCallback(
$callback,
[$carry],
'%s is not trusted',
),
$props
);
$inline_template = $this->generateComponentTemplate(
$element['#component'],
$element['#slots'],
$element['#slotsAlter'],
$props,
);
$element['inline-template'] = [
'#type' => 'inline_template',
'#template' => $inline_template,
'#context' => $props,
];
return $element;
}
/**
* {@inheritdoc}
*
* Related SDC issue: https://www.drupal.org/project/drupal/issues/3391702
*/
private function generateComponentTemplate(
string $id,
array $slots,
array $slots_alter_callbacks,
array &$context,
): string {
$template = '{# This template was dynamically generated by sdc #}' . PHP_EOL;
$template .= sprintf('{%% embed \'%s\' %%}', $id);
$template .= PHP_EOL;
foreach ($slots as $slot_name => $slot_value) {
if (!Utilities::isRenderArray($slot_value) && \is_scalar($slot_value)) {
$slot_value = [
"#plain_text" => (string) $slot_value,
];
}
$context[$slot_name] = array_reduce(
$slots_alter_callbacks,
fn(array $carry, callable $callback) => $this->doTrustedCallback(
$callback,
[$carry, $context],
'%s is not trusted',
),
$slot_value
);
$template .= " {% block $slot_name %}" . PHP_EOL
. " {{ $slot_name }}" . PHP_EOL
. " {% endblock %}" . PHP_EOL;
}
$template .= '{% endembed %}' . PHP_EOL;
return $template;
}
}
<?php
namespace Drupal\ui_patterns\Element;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Template\Attribute;
use Drupal\ui_patterns\UiPatterns;
/**
* Renders a pattern element.
*
* @RenderElement("pattern")
*/
class Pattern extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => FALSE,
'#multiple_sources' => FALSE,
'#pre_render' => [
[$class, 'processContext'],
[$class, 'processRenderArray'],
[$class, 'processLibraries'],
[$class, 'processMultipleSources'],
[$class, 'processFields'],
[$class, 'ensureVariant'],
[$class, 'processUse'],
],
];
}
/**
* Process render array.
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*/
public static function processRenderArray(array $element) {
$element['#theme'] = UiPatterns::getPatternDefinition($element['#id'])->getThemeHook();
if (isset($element['#attributes']) && !empty($element['#attributes']) && is_array($element['#attributes'])) {
$element['#attributes'] = new Attribute($element['#attributes']);
}
else {
$element['#attributes'] = new Attribute();
}
unset($element['#type']);
return $element;
}
/**
* Process libraries.
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*/
public static function processLibraries(array $element) {
foreach (UiPatterns::getPatternDefinition($element['#id'])->getLibrariesNames() as $library) {
$element['#attached']['library'][] = $library;
}
return $element;
}
/**
* Process fields.
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*/
public static function processFields(array $element) {
// Make sure we don't render anything in case fields are empty.
if (self::hasFields($element)) {
$fields = $element['#fields'];
unset($element['#fields']);
foreach ($fields as $name => $field) {
$key = '#' . $name;
$element[$key] = $field;
}
}
else {
$element['#markup'] = '';
}
return $element;
}
/**
* Make sure that we never pass through a value that is not a string.
*
* This would prevent accidental assignments of a render array as variant
* which would break hook_ui_patterns_suggestions_alter().
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*/
public static function ensureVariant(array $element) {
if (!isset($element['#variant']) || !is_string($element['#variant'])) {
$element['#variant'] = '';
}
return $element;
}
/**
* Process use property.
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*/
public static function processUse(array $element) {
$definition = UiPatterns::getPatternDefinition($element['#id']);
if ($definition->hasUse()) {
$element['#use'] = $definition->getUse();
}
if (isset($element['#variant'])) {
$variant_definition = $definition->getVariant($element['#variant']);
if ($variant_definition && $variant_definition->hasUse()) {
$element['#use'] = $variant_definition->getUse();
}
}
return $element;
}
/**
* Process fields.
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*/
public static function processMultipleSources(array $element) {
// Make sure we don't render anything in case fields are empty.
if (self::hasFields($element) && self::hasMultipleSources($element)) {
foreach ($element['#fields'] as $name => $field) {
// This guarantees backward compatibility: single sources be simple.
$element['#fields'][$name] = reset($field);
if (count($field) > 1) {
/** @var \Drupal\ui_patterns\Element\PatternContext $context */
$context = $element['#context'];
$context->setProperty('pattern', $element['#id']);
$context->setProperty('field', $name);
// Render multiple sources with "patterns_destination" template.
$element['#fields'][$name] = [
'#sources' => $field,
'#context' => $context,
'#theme' => 'patterns_destination',
];
}
}
}
return $element;
}
/**
* Process context.
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*
* @throws \Drupal\ui_patterns\Exception\PatternRenderException
* Throws an exception if no context type is specified.
*/
public static function processContext(array $element) {
if (self::hasValidContext($element)) {
$context = $element['#context'];
$element['#context'] = new PatternContext($context['type'], $element['#context']);
}
else {
$element['#context'] = new PatternContext('empty');
}
return $element;
}
/**
* Whereas pattern has field or not.
*
* @param array $element
* Render array.
*
* @return bool
* TRUE or FALSE.
*/
public static function hasFields(array $element) {
return isset($element['#fields']) && !empty($element['#fields']) && is_array($element['#fields']);
}
/**
* Whereas pattern fields can accept multiple sources.
*
* @param array $element
* Render array.
*
* @return bool
* TRUE or FALSE.
*/
public static function hasMultipleSources(array $element) {
return isset($element['#multiple_sources']) && $element['#multiple_sources'] === TRUE;
}
/**
* Whereas pattern has a valid context, i.e. context "type" is set.
*
* @param array $element
* Render array.
*
* @return bool
* TRUE or FALSE.
*/
public static function hasValidContext(array $element) {
return isset($element['#context']) && is_array($element['#context']) && !empty($element['#context']['type']);
}
}
<?php
namespace Drupal\ui_patterns\Element;
/**
* Represent the context in which the pattern is being rendered.
*
* @package Drupal\ui_patterns\Context
*/
class PatternContext {
/**
* Pattern context type.
*
* @var string
*/
protected $type = '';
/**
* Context properties.
*
* @var array
*/
protected $properties = [];
/**
* PatternContext constructor.
*
* @param string $type
* Pattern context type.
* @param array $values
* Initial context values.
*/
public function __construct($type, array $values = []) {
$this->type = $type;
unset($values['type']);
foreach ($values as $name => $value) {
$this->setProperty($name, $value);
}
}
/**
* Get pattern context property.
*
* @return mixed
* Property value.
*/
public function getProperty($name) {
return $this->properties[$name] ?? NULL;
}
/**
* Set pattern context property.
*
* @param string $name
* Property name.
* @param mixed $value
* Property value.
*/
public function setProperty($name, $value) {
$this->properties[$name] = $value;
}
/**
* Check whereas the current context is of a given type.
*
* @param string $type
* Type string.
*
* @return bool
* Whereas the current context is of a given type.
*/
public function isOfType($type) {
return $this->type == $type;
}
/**
* Get context type.
*
* @return string
* Context type.
*/
public function getType() {
return $this->type;
}
}
<?php
namespace Drupal\ui_patterns\Element;
use Drupal\Core\Render\Markup;
use Drupal\ui_patterns\UiPatterns;
/**
* Renders a pattern preview element.
*
* @RenderElement("pattern_preview")
*/
class PatternPreview extends Pattern {
/**
* Process fields.
*
* @param array $element
* Render array.
*
* @return array
* Render array.
*/
public static function processFields(array $element) {
$definition = UiPatterns::getPatternDefinition($element['#id']);
$fields = [];
foreach ($definition->getFields() as $field) {
$fields[$field->getName()] = $field->getPreview();
}
if (isset($definition['additional']['attributes'])) {
$fields['attributes'] = $definition['extra']['attributes'];
}
$element['#fields'] = $fields;
return parent::processFields($element);
}
/**
* {@inheritdoc}
*/
public static function processContext(array $element) {
$element['#context'] = new PatternContext('preview');
return $element;
}
}
<?php
namespace Drupal\ui_patterns\Exception;
use Drupal\Component\Plugin\Exception\PluginException;
/**
* Exception thrown in case ofan invalid pattern definition.
*
* @package Drupal\ui_patterns\Exception
*/
class PatternDefinitionException extends PluginException {
}
<?php
namespace Drupal\ui_patterns\Exception;
/**
* Exception thrown in case of an invalid pattern rendering.
*
* @package Drupal\ui_patterns\Exception
*/
class PatternRenderException extends \Exception {
}
<?php
declare(strict_types = 1);
namespace Drupal\ui_patterns\Template;
use Drupal\Core\Template\Attribute;
......@@ -31,7 +33,7 @@ trait AttributesFilterTrait {
if (!\is_array($element)) {
return $element;
}
if ($this->arrayIsList($element)) {
if (\array_is_list($element)) {
foreach ($element as $index => $item) {
if (!\is_array($item)) {
continue;
......@@ -70,7 +72,7 @@ trait AttributesFilterTrait {
if (!\is_array($element)) {
return $element;
}
if ($this->arrayIsList($element)) {
if (\array_is_list($element)) {
foreach ($element as $index => $item) {
if (!\is_array($item)) {
continue;
......@@ -90,28 +92,4 @@ trait AttributesFilterTrait {
return $element;
}
/**
* Checks whether a given array is a list.
*
* Same as array_is_list() but compatible with PHP8.
*
* @param array $array
* The array being evaluated.
*
* @return bool
* Returns true if array is a list, false otherwise.
*
* @see https://www.php.net/manual/en/function.array-is-list.php#126794
*/
private function arrayIsList(array $array): bool {
$i = -1;
foreach ($array as $k => $v) {
++$i;
if ($k !== $i) {
return FALSE;
}
}
return TRUE;
}
}
<?php
declare(strict_types = 1);
namespace Drupal\ui_patterns\Template;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* Twig extension providing UI Patterns-specific functionalities.
......@@ -27,13 +29,9 @@ class TwigExtension extends AbstractExtension {
*/
public function getFunctions() {
return [
new TwigFunction('pattern', [
$this,
'renderPattern',
]),
new TwigFunction('pattern_preview', [
new TwigFunction('component', [
$this,
'renderPatternPreview',
'renderComponent',
]),
];
}
......@@ -49,47 +47,26 @@ class TwigExtension extends AbstractExtension {
}
/**
* Render given pattern.
*
* @param string $id
* Pattern ID.
* @param array $fields
* Pattern fields.
* @param string $variant
* Variant name.
*
* @return array
* Pattern render array.
*
* @see \Drupal\ui_patterns\Element\Pattern
*/
public function renderPattern($id, array $fields = [], $variant = "") {
return [
'#type' => 'pattern',
'#id' => $id,
'#fields' => $fields,
'#variant' => $variant,
];
}
/**
* Render given pattern.
* Render given component.
*
* @param string $id
* Pattern ID.
* @param string $variant
* Variant name.
* @param string $component_id
* Component ID.
* @param array $slots
* Pattern slots.
* @param array $props
* Pattern props.
*
* @return array
* Pattern render array.
*
* @see \Drupal\ui_patterns\Element\Pattern
* @see \Drupal\sdc\Element\ComponentElement
*/
public function renderPatternPreview($id, $variant = "") {
public function renderComponent(string $component_id, array $slots = [], array $props = []) {
return [
'#type' => 'pattern_preview',
'#id' => $id,
'#variant' => $variant,
'#type' => 'component',
'#component' => $component_id,
'#slots' => $slots,
'#props' => $props,
];
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment