Skip to content
Snippets Groups Projects
Commit c490a366 authored by Wim Leers's avatar Wim Leers Committed by Ted Bowman
Browse files

Issue #3463986 by Wim Leers: Refactor SdcController::preview() to use...

Issue #3463986 by Wim Leers: Refactor SdcController::preview() to use ComponentTreeHydrated, and update it to support nested components
parent 1c224d27
No related branches found
No related tags found
1 merge request!128Resolve #3463986 "Preview componenttreehydrated"
Pipeline #239781 passed
......@@ -19,6 +19,7 @@ toolset
topbar
Validatable
drupalcode
renderify
subcontent
subtrigger
testid
......
......@@ -6,13 +6,14 @@ namespace Drupal\experience_builder\Controller;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\experience_builder\FieldForComponentSuggester;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Theme\ComponentPluginManager;
use Drupal\experience_builder\Entity\Component;
use Drupal\experience_builder\Plugin\DataType\ComponentTreeHydrated;
use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure;
use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem;
......@@ -26,6 +27,17 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
// phpcs:disable
// @todo Remove this — this was added to avoid breaking the client while finalizing the server.
final class HardcodedPropsComponentTreeItem extends ComponentTreeItem {
public array $hardcoded_props = [];
public function resolveComponentProps(string $component_instance_uuid): array {
// @todo the current quick-and-dirty UI PoC unfortunately prevents any prop from being named `name`, because it expects that to convey the component name — but it's not actually one of the props consumed by the SDC.
return array_diff_key($this->hardcoded_props[$component_instance_uuid], ['name' => NULL]);
}
}
// phpcs:enable
final class SdcController extends ControllerBase {
/**
......@@ -43,6 +55,7 @@ final class SdcController extends ControllerBase {
protected AssetResolverInterface $assetResolver,
protected AssetCollectionRendererInterface $cssCollectionRenderer,
protected AssetCollectionRendererInterface $jsCollectionRenderer,
private readonly TypedDataManagerInterface $typedDataManager,
) {}
/**
......@@ -56,6 +69,7 @@ final class SdcController extends ControllerBase {
$container->get('asset.resolver'),
$container->get('asset.css.collection_renderer'),
$container->get('asset.js.collection_renderer'),
$container->get(TypedDataManagerInterface::class)
);
}
......@@ -200,7 +214,7 @@ final class SdcController extends ControllerBase {
$hydrated_json = $hydrated->getValue()->getContent();
assert(is_string($hydrated_json));
// @todo tree recursion/slot support — this only supports a flat list — blocked on https://www.drupal.org/project/experience_builder/issues/3455728
// @todo tree recursion/slot support — this only supports a flat list — do this in https://www.drupal.org/project/experience_builder/issues/3446722
$children = [];
foreach (json_decode($tree->getValue(), TRUE)[ComponentTreeStructure::ROOT_UUID] as ['uuid' => $component_instance_uuid, 'component' => $component_type]) {
$children[] = [
......@@ -212,7 +226,7 @@ final class SdcController extends ControllerBase {
}
$model = [];
foreach (json_decode($hydrated_json, TRUE) as $component_instance_uuid => ['props' => $resolved_prop_values]) {
foreach (json_decode($hydrated_json, TRUE)[ComponentTreeStructure::ROOT_UUID] as $component_instance_uuid => ['props' => $resolved_prop_values]) {
$model[$component_instance_uuid] = $resolved_prop_values;
$component_id = $tree->getComponentId($component_instance_uuid);
// @todo the current quick-and-dirty UI PoC unfortunately prevents any prop from being named `name`, because it expects that to convey the component name
......@@ -239,28 +253,77 @@ final class SdcController extends ControllerBase {
]);
}
public function preview(Request $request): JsonResponse {
$component_list = array_map(fn($component) => $component->getPluginId(), $this->componentPluginManager->getAllComponents());
['layout' => $layout, 'model' => $model] = json_decode($request->getContent(), TRUE);
$component_and_other_names = [];
private static function clientLayoutToServerTree(array $layout, string $parent_uuid, ?string $parent_slot, array &$tree) : void {
foreach ($layout['children'] as $child) {
if ($child['nodeType'] === 'slot') {
// @todo This indicates the client model does not quite make sense: SDC slots do NOT have UUIDs, but names!
self::clientLayoutToServerTree($child, $parent_uuid, $child['uuid'], $tree);
continue;
}
// Get all components in the layout, at any depth.
array_walk_recursive($layout, function ($value, $key) use (&$component_and_other_names) {
if ($key == 'type') {
$component_and_other_names[] = $value;
// Root level.
if (!isset($parent_slot)) {
$tree[$parent_uuid][] = [
'uuid' => $child['uuid'],
'component' => $child['type'],
];
}
});
// All other levels.
else {
$tree[$parent_uuid][$parent_slot][] = [
'uuid' => $child['uuid'],
'component' => $child['type'],
];
}
}
}
// Remove duplicates and non-components.
$components_in_use = array_filter(array_unique($component_and_other_names), fn($item) => in_array($item, $component_list, TRUE));
/**
* Transform the `layout` + `model` data structure that the client uses.
*
* This is the server side, so transform to the representation used by the
* server-side field type. This allows reusing all of the field type
* infrastructure, which is also used for
* final rendering.
*
* @see \Drupal\experience_builder\Plugin\Field\FieldFormatter\NaiveComponentTreeFormatter
*/
private function clientLayoutAndModelToXbField(array $layout, array $model): ComponentTreeItem {
$field_item_definition = $this->typedDataManager->createDataDefinition('field_item:component_tree');
// @phpstan-ignore-next-line
$field_item_definition->setClass(HardcodedPropsComponentTreeItem::class);
$component_tree_field_item = $this->typedDataManager->createInstance('field_item:component_tree', [
'name' => NULL,
'parent' => NULL,
'data_definition' => $field_item_definition,
]);
$assets = AttachedAssets::createFromRenderArray([
'#attached' => [
// @see \Drupal\Core\Plugin\Component::getLibraryName()
'library' => array_map(fn($name) => 'core/components.' . str_replace(':', '--', $name), $components_in_use),
],
// Transform `layout` to `tree`.
// @see \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure
$tree = [ComponentTreeStructure::ROOT_UUID => []];
self::clientLayoutToServerTree($layout, ComponentTreeStructure::ROOT_UUID, NULL, $tree);
// This uses a partial override of the XB field type, because the client is
// sending explicit prop values in its `model`, not prop sources. Use these
// directly.
// @see \Drupal\experience_builder\Controller\HardcodedPropsComponentTreeItem::resolveComponentProps()
assert($component_tree_field_item instanceof HardcodedPropsComponentTreeItem);
$component_tree_field_item->setValue([
'tree' => json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT),
]);
$component_tree_field_item->hardcoded_props = $model;
return $component_tree_field_item;
}
public function preview(Request $request): JsonResponse {
['layout' => $layout, 'model' => $model] = json_decode($request->getContent(), TRUE);
$component_tree_field_item = $this->clientLayoutAndModelToXbField($layout, $model);
$build = self::wrapComponentsForPreview($component_tree_field_item->toRenderable());
$this->renderer->renderInIsolation($build);
$assets = AttachedAssets::createFromRenderArray($build);
$css_array = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, FALSE));
[$head_assets, $foot_assets] = $this->assetResolver->getJsAssets($assets, FALSE);
$head_array = $this->jsCollectionRenderer->render($head_assets);
......@@ -287,21 +350,7 @@ HTML;
<body>
<div class="sortable-list" data-xb-uuid="root">
HTML;
// @todo tree recursion — this only supports a flat list
// @todo Refactor to use \Drupal\experience_builder\Plugin\DataType\ComponentTreeHydrated.
foreach ($layout['children'] as ['uuid' => $uuid, 'type' => $type]) {
$html .= sprintf('<div class="sortable-item" data-xb-uuid="%s" data-xb-type="%s">', $uuid, $type);
// @todo the current quick-and-dirty UI PoC unfortunately prevents any prop from being named `name`, because it expects that to convey the component name — but it's not actually one of the props consumed by the SDC.
unset($model[$uuid]['name']);
$build = [
'#type' => 'component',
'#component' => $type,
'#props' => $model[$uuid],
];
// @todo support CSS + JS
$html .= $this->renderer->renderInIsolation($build);
$html .= '</div>';
}
$html .= $build['#markup'];
$html .= <<<HTML
</body>
HTML;
......@@ -315,6 +364,18 @@ HTML;
]);
}
private static function wrapComponentsForPreview(array $build, ?string $component_instance_uuid = NULL): array {
if (isset($build['#component'])) {
assert(is_string($component_instance_uuid));
$build['#prefix'] = sprintf('<div class="sortable-item" data-xb-uuid="%s" data-xb-type="%s">', $component_instance_uuid, $build['#component']);
$build['#suffix'] = '</div>';
}
foreach (Element::children($build) as $component_instance_uuid) {
$build[$component_instance_uuid] = self::wrapComponentsForPreview($build[$component_instance_uuid], $component_instance_uuid);
}
return $build;
}
/**
* Assign values to props in the SDC render array.
*
......
......@@ -34,21 +34,32 @@ class ComponentTreeHydrated extends TypedData implements CacheableDependencyInte
assert($tree instanceof ComponentTreeStructure);
$hydrated = [];
// Hydrate all component instances, but only considering props. This
// essentially means getting the values for each component instance, while
// ignoring their slots. The result: a flat list of hydrated components, but
// with all slots empty.
foreach ($tree->getComponentInstanceUuids() as $uuid) {
$sdc_component_id = $tree->getComponentId($uuid);
$sdc_component_props = $item->resolveComponentProps($uuid);
$hydrated[$uuid] = [
'component' => $sdc_component_id,
'props' => $sdc_component_props,
'slots' => [
// @todo support nesting!
],
];
}
// Transform the flat list of hydrated components into a hydrated component
// tree, by assigning child components to their parent component's slot. If
// this happens depth-first, then the tree will gradually be built, with the
// last iteration assigning the last component to the component tree's root.
foreach ($tree->getSlotChildrenDepthFirst() as $parent_uuid => ['slot' => $slot, 'uuid' => $uuid]) {
$hydrated[$parent_uuid]['slots'][$slot][$uuid] = $hydrated[$uuid];
unset($hydrated[$uuid]);
}
return (new CacheableJsonResponse())
->addCacheableDependency($this->getCacheability())
->setData($hydrated);
->setData([ComponentTreeStructure::ROOT_UUID => $hydrated]);
}
/**
......@@ -68,8 +79,6 @@ class ComponentTreeHydrated extends TypedData implements CacheableDependencyInte
// the source of truth. So we start from a Drupal Render API-agnostic point,
// and map that into a render array. This guarantees none of this will ever
// rely on Render API specifics.
// Note: see commented out code below for the "direct render array"
// equivalent.
$renderable_component_tree = $this->getValue();
$build = [];
......@@ -80,43 +89,38 @@ class ComponentTreeHydrated extends TypedData implements CacheableDependencyInte
assert(is_string($json));
$hydrated = json_decode($json, TRUE);
$build = [];
foreach ($hydrated as $uuid => $values) {
$build[$uuid] = [
'#type' => 'component',
];
foreach ($values as $key => $value) {
$build[$uuid]["#$key"] = $value;
}
}
return $build;
// phpcs:disable
/*
$item = $this->getParent();
assert($item instanceof ComponentTreeItem);
$tree = $item->get('tree');
assert($tree instanceof ComponentTreeStructure);
assert(array_keys($hydrated) === [ComponentTreeStructure::ROOT_UUID]);
return self::renderify($hydrated);
}
/**
* Recursively converts an array generated by ::getValue() to a render array.
*
* @param array $hydrated
* An array generated by ::getValue().
*
* @return array
* The corresponding render array.
*/
private static function renderify(array $hydrated) {
$build = [];
// @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults()
$this->getCacheability()->applyTo($build);
foreach ($tree->getComponentInstanceUuids() as $uuid) {
$sdc_component_id = $tree->getComponentId($uuid);
$sdc_component_props = $item->resolveComponentProps($uuid);
$build[$uuid] = [
'#type' => 'component',
'#component' => $sdc_component_id,
'#props' => $sdc_component_props,
'#slots' => [
// @todo support nesting!
],
];
foreach ($hydrated as $component_subtree_uuid => $component_instances) {
foreach ($component_instances as $component_instance_uuid => $component_instance) {
$build[$component_subtree_uuid][$component_instance_uuid] = [
'#type' => 'component',
];
foreach ($component_instance as $key => $value) {
$build[$component_subtree_uuid][$component_instance_uuid]["#$key"] = $key !== 'slots'
// Note: this works because `::getValue()` above uses `props` and
// `slots`, which allow simply prefixing them with `#` to generate the
// corresponding render array.
? $value
// The exception is the `slots` key: this one needs recursion.
: self::renderify($value);
}
}
}
return $build;
*/
// phpcs:enable
}
/**
......@@ -130,8 +134,12 @@ class ComponentTreeHydrated extends TypedData implements CacheableDependencyInte
// @see \Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem::preSave()
$root = $this->getRoot();
assert($root instanceof EntityAdapter);
return CacheableMetadata::createFromObject($root->getEntity());
if ($root instanceof EntityAdapter) {
return CacheableMetadata::createFromObject($root->getEntity());
}
// This appears to be an ephemeral component tree, hence it is uncacheable.
return (new CacheableMetadata())->setCacheMaxAge(0);
}
/**
......
......@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace Drupal\experience_builder\Plugin\DataType;
use Drupal\Component\Graph\Graph;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\Attribute\DataType;
use Drupal\Core\TypedData\TypedData;
......@@ -82,11 +84,15 @@ class ComponentTreeStructure extends TypedData {
* Because all config entities have a corresponding Component plugin, and it is not possible to have 2 config entities that relate to the same plugin, this works.
* It is a bit confusing but probably not worth fixing as this will all change in https://drupal.org/i/3454519.
*
* @var array<string,array<int, array{'uuid': string, 'component': string}>|array<string, array<int, array{'uuid': string, 'component': string}>>
* >
* @var array<string,array<int, array{'uuid': string, 'component': string}>|array<string, array<int, array{'uuid': string, 'component': string}>>>
*/
protected array $tree = [];
/**
* @var null|array<string, array{'edges': array<string, TRUE>}>
*/
protected ?array $graph = NULL;
/**
* {@inheritdoc}
*/
......@@ -113,6 +119,10 @@ class ComponentTreeStructure extends TypedData {
// @todo Delete next line; update this code to ONLY do the JSON-to-PHP-object parsing after https://www.drupal.org/project/drupal/issues/2232427 lands — that will allow specifying the "json" serialization strategy rather than only PHP's serialize().
$this->value = $value;
$this->tree = Json::decode($value);
// Keep the graph representation in sync: force it to be recomputed.
$this->graph = NULL;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
......@@ -162,6 +172,73 @@ class ComponentTreeStructure extends TypedData {
return $components;
}
/**
* Constructs a depth-first graph based on the given tree.
*
* @param array<string,array<int, array{'uuid': string, 'component': string}>|array<string, array<int, array{'uuid': string, 'component': string}>>> $tree
*
* @return array<string, array{'edges': array<string, TRUE>}>
*
* @see \Drupal\Component\Graph\Graph
*/
private static function constructDepthFirstGraph(array $tree): array {
// Transform the tree to the input expected by Drupal's Graph utility.
$graph = [];
foreach ($tree as $component_subtree_uuid => $value) {
if ($component_subtree_uuid === self::ROOT_UUID) {
foreach (array_column($value, 'uuid') as $component_instance_uuid) {
assert(is_string($component_instance_uuid));
$graph[$component_subtree_uuid]['edges'][$component_instance_uuid] = TRUE;
}
continue;
}
foreach ($value as $slot => $component_instances) {
$graph[$component_subtree_uuid]['edges']["$component_subtree_uuid:$slot"] = TRUE;
foreach (array_column($component_instances, 'uuid') as $component_instance_uuid) {
$graph["$component_subtree_uuid:$slot"]['edges'][$component_instance_uuid] = TRUE;
}
}
}
// Use Drupal's battle-hardened Graph utility.
$sorted_graph = (new Graph($graph))->searchAndSort();
// Sort by weight, then reverse: this results in a depth-first sorted graph.
uasort($sorted_graph, [SortArray::class, 'sortByWeightElement']);
$reverse_sorted_graph = array_reverse($sorted_graph);
return $reverse_sorted_graph;
}
/**
* @return \Generator<string, array{'slot': string, 'uuid': string}>
*/
public function getSlotChildrenDepthFirst(): \Generator {
if ($this->graph === NULL) {
$this->graph = self::constructDepthFirstGraph($this->tree);
}
foreach ($this->graph as $vertex_key => $vertex) {
// This method is concerned only with component instances in slots. Those
// are easily identified by their vertex key: they must contain a colon,
// which separates the parent component instance UUID from the slot name.
if (!str_contains($vertex_key, ':')) {
continue;
}
[$parent_uuid, $slot] = explode(':', $vertex_key, 2);
// For each vertex (after the filtering above), all edges represent
// child component instances placed in this slot.
foreach (array_keys($vertex['edges']) as $component_instance_uuid) {
assert(is_string($component_instance_uuid));
yield $parent_uuid => [
'slot' => $slot,
'uuid' => $component_instance_uuid,
];
}
}
}
/**
* @param string $component_instance_uuid
* The UUID of a placed component instance.
......
......@@ -292,53 +292,50 @@ class EndToEndDemoIntegrationTest extends BrowserTestBase {
$hydrated = $node->get('field_xb_demo')[0]->get('hydrated');
$this->assertInstanceOf(ComponentTreeHydrated::class, $hydrated);
$this->assertEquals([
'dynamic-image-udf7d' => [
'component' => 'experience_builder:image',
'props' => [
'image' => [
'src' => File::load(1)->getFileUri(),
'alt' => 'A random image for testing purposes.',
'width' => 40,
'height' => 20,
ComponentTreeStructure::ROOT_UUID => [
'dynamic-image-udf7d' => [
'component' => 'experience_builder:image',
'props' => [
'image' => [
'src' => File::load(1)->getFileUri(),
'alt' => 'A random image for testing purposes.',
'width' => 40,
'height' => 20,
],
],
],
'slots' => [],
],
'static-static-card1ab' => [
'component' => 'experience_builder:my-hero',
'props' => [
'heading' => 'hello, world!',
'cta1href' => 'https://drupal.org',
'static-static-card1ab' => [
'component' => 'experience_builder:my-hero',
'props' => [
'heading' => 'hello, world!',
'cta1href' => 'https://drupal.org',
],
],
'slots' => [],
],
'dynamic-static-card2df' => [
'component' => 'experience_builder:my-hero',
'props' => [
'heading' => $node->getTitle(),
'cta1href' => 'https://drupal.org',
'dynamic-static-card2df' => [
'component' => 'experience_builder:my-hero',
'props' => [
'heading' => $node->getTitle(),
'cta1href' => 'https://drupal.org',
],
],
'slots' => [],
],
'dynamic-dynamic-card3rr' => [
'component' => 'experience_builder:my-hero',
'props' => [
'heading' => $node->getTitle(),
'cta1href' => File::load(1)->getFileUri(),
'dynamic-dynamic-card3rr' => [
'component' => 'experience_builder:my-hero',
'props' => [
'heading' => $node->getTitle(),
'cta1href' => File::load(1)->getFileUri(),
],
],
'slots' => [],
],
'dynamic-image-static-imageStyle-something7d' => [
'component' => 'experience_builder:image',
'props' => [
'image' => [
'src' => ImageStyle::load('thumbnail')->buildUrl(File::load(1)->getFileUri()),
'alt' => 'A random image for testing purposes.',
'width' => 40,
'height' => 20,
'dynamic-image-static-imageStyle-something7d' => [
'component' => 'experience_builder:image',
'props' => [
'image' => [
'src' => ImageStyle::load('thumbnail')->buildUrl(File::load(1)->getFileUri()),
'alt' => 'A random image for testing purposes.',
'width' => 40,
'height' => 20,
],
],
],
'slots' => [],
],
// @phpstan-ignore-next-line
], json_decode($hydrated->getValue()->getContent(), TRUE));
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\experience_builder\Kernel\DataType;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\experience_builder\Plugin\DataType\ComponentTreeHydrated;
use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure;
use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\experience_builder\Traits\ConstraintViolationsTestTrait;
/**
* @coversDefaultClass \Drupal\experience_builder\Plugin\DataType\ComponentTreeHydrated
* @group experience_builder
*/
class ComponentTreeHydratedTest extends KernelTestBase {
use ConstraintViolationsTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'experience_builder',
'xb_test_sdc',
];
/**
* @dataProvider provider
*/
public function test(array $tree, array $props, array $expected_value, array $expected_renderable, string $expected_html): void {
$typed_data_manager = $this->container->get(TypedDataManagerInterface::class);
$field_item_definition = $typed_data_manager->createDataDefinition('field_item:component_tree');
$component_tree_field_item = $typed_data_manager->createInstance('field_item:component_tree', [
'name' => NULL,
'parent' => NULL,
'data_definition' => $field_item_definition,
]);
assert($component_tree_field_item instanceof ComponentTreeItem);
$component_tree_field_item->setValue([
'tree' => json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT),
'props' => json_encode($props, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT),
]);
// Every test case must be valid.
$violations = $component_tree_field_item->validate();
$this->assertSame([], self::violationsToArray($violations));
// Assert that the corresponding hydrated component tree is valid, in both
// representations:
// 1. raw (`::getValue()`)
// 2. Drupal renderable (`::toRenderable()`)
// 3. the resulting HTML markup.assert($node->field_xb_test[0] instanceof ComponentTreeItem);
$hydrated = $component_tree_field_item->get('hydrated');
assert($hydrated instanceof ComponentTreeHydrated);
$hydrated_value = $hydrated->getValue();
$json = $hydrated_value->getContent();
$this->assertIsString($json);
$this->assertSame($expected_value, json_decode($json, TRUE));
$renderable = $hydrated->toRenderable();
$this->assertSame($expected_renderable, $renderable);
$this->assertSame($expected_html, (string) $this->container->get(RendererInterface::class)->renderInIsolation($renderable));
}
public static function provider(): \Generator {
$generate_static_prop_source = function (string $label): array {
return [
'sourceType' => 'static:field_item:string',
'value' => "Hello, $label!",
'expression' => 'ℹ︎string␟value',
];
};
yield 'empty component tree' => [
'tree structure' => [
ComponentTreeStructure::ROOT_UUID => [],
],
'props values' => [],
'expected value' => [
ComponentTreeStructure::ROOT_UUID => [],
],
'expected renderable' => [],
'expected HTML' => '',
];
yield 'simplest component tree without nesting' => [
'tree structure' => [
ComponentTreeStructure::ROOT_UUID => [
['uuid' => 'uuid-in-root', 'component' => 'xb_test_sdc:props-no-slots'],
['uuid' => 'uuid-in-root-another', 'component' => 'xb_test_sdc:props-no-slots'],
],
],
'props values' => [
'uuid-in-root' => [
'heading' => $generate_static_prop_source('world'),
],
'uuid-in-root-another' => [
'heading' => $generate_static_prop_source('another world'),
],
],
'expected value' => [
ComponentTreeStructure::ROOT_UUID => [
'uuid-in-root' => [
'component' => 'xb_test_sdc:props-no-slots',
'props' => ['heading' => 'Hello, world!'],
],
'uuid-in-root-another' => [
'component' => 'xb_test_sdc:props-no-slots',
'props' => ['heading' => 'Hello, another world!'],
],
],
],
'expected renderable' => [
ComponentTreeStructure::ROOT_UUID => [
'uuid-in-root' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-no-slots',
'#props' => ['heading' => 'Hello, world!'],
],
'uuid-in-root-another' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-no-slots',
'#props' => ['heading' => 'Hello, another world!'],
],
],
],
'expected HTML' => <<<HTML
<div data-component-id="xb_test_sdc:props-no-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, world!</h1>
</div>
<div data-component-id="xb_test_sdc:props-no-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, another world!</h1>
</div>
HTML,
];
yield 'simplest component tree with nesting' => [
'tree structure' => [
ComponentTreeStructure::ROOT_UUID => [
['uuid' => 'uuid-in-root', 'component' => 'xb_test_sdc:props-slots'],
],
'uuid-in-root' => [
'the_body' => [
['uuid' => 'uuid-in-slot', 'component' => 'xb_test_sdc:props-no-slots'],
],
],
],
'props values' => [
'uuid-in-root' => [
'heading' => $generate_static_prop_source('world'),
],
'uuid-in-slot' => [
'heading' => $generate_static_prop_source('from a slot'),
],
],
'expected value' => [
ComponentTreeStructure::ROOT_UUID => [
'uuid-in-root' => [
'component' => 'xb_test_sdc:props-slots',
'props' => ['heading' => 'Hello, world!'],
'slots' => [
'the_body' => [
'uuid-in-slot' => [
'component' => 'xb_test_sdc:props-no-slots',
'props' => ['heading' => 'Hello, from a slot!'],
],
],
],
],
],
],
'expected renderable' => [
ComponentTreeStructure::ROOT_UUID => [
'uuid-in-root' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-slots',
'#props' => ['heading' => 'Hello, world!'],
'#slots' => [
'the_body' => [
'uuid-in-slot' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-no-slots',
'#props' => ['heading' => 'Hello, from a slot!'],
],
],
],
],
],
],
'expected HTML' => <<<HTML
<div data-component-id="xb_test_sdc:props-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, world!</h1>
<div class="component--props-slots--body">
<div data-component-id="xb_test_sdc:props-no-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, from a slot!</h1>
</div>
</div>
</div>
HTML,
];
yield 'component tree with complex nesting' => [
'tree structure' => [
// Note how these are NOT sequentially ordered.
'uuid-in-root' => [
'the_body' => [
['uuid' => 'uuid-level-1', 'component' => 'xb_test_sdc:props-slots'],
],
],
'uuid-level-2' => [
'the_body' => [
['uuid' => 'uuid-level-3', 'component' => 'xb_test_sdc:props-no-slots'],
['uuid' => 'uuid-last-in-tree', 'component' => 'xb_test_sdc:props-no-slots'],
],
],
'uuid-level-1' => [
'the_body' => [
['uuid' => 'uuid-level-2', 'component' => 'xb_test_sdc:props-slots'],
],
],
ComponentTreeStructure::ROOT_UUID => [
['uuid' => 'uuid-in-root', 'component' => 'xb_test_sdc:props-slots'],
],
],
'props values' => [
// Note how these are NOT sequentially ordered, but in a different way.
'uuid-in-root' => [
'heading' => $generate_static_prop_source('world'),
],
'uuid-level-3' => ['heading' => $generate_static_prop_source('from slot level 3')],
'uuid-level-1' => ['heading' => $generate_static_prop_source('from slot level 1')],
'uuid-last-in-tree' => ['heading' => $generate_static_prop_source('from slot <LAST ONE>')],
'uuid-level-2' => ['heading' => $generate_static_prop_source('from slot level 2')],
],
'expected value' => [
// Note how these are sequentially ordered.
ComponentTreeStructure::ROOT_UUID => [
'uuid-in-root' => [
'component' => 'xb_test_sdc:props-slots',
'props' => ['heading' => 'Hello, world!'],
'slots' => [
'the_body' => [
'uuid-level-1' => [
'component' => 'xb_test_sdc:props-slots',
'props' => ['heading' => 'Hello, from slot level 1!'],
'slots' => [
'the_body' => [
'uuid-level-2' => [
'component' => 'xb_test_sdc:props-slots',
'props' => ['heading' => 'Hello, from slot level 2!'],
'slots' => [
'the_body' => [
'uuid-level-3' => [
'component' => 'xb_test_sdc:props-no-slots',
'props' => ['heading' => 'Hello, from slot level 3!'],
],
'uuid-last-in-tree' => [
'component' => 'xb_test_sdc:props-no-slots',
'props' => ['heading' => 'Hello, from slot <LAST ONE>!'],
],
],
],
],
],
],
],
],
],
],
],
],
'expected renderable' => [
// Note how these are sequentially ordered.
ComponentTreeStructure::ROOT_UUID => [
'uuid-in-root' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-slots',
'#props' => ['heading' => 'Hello, world!'],
'#slots' => [
'the_body' => [
'uuid-level-1' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-slots',
'#props' => ['heading' => 'Hello, from slot level 1!'],
'#slots' => [
'the_body' => [
'uuid-level-2' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-slots',
'#props' => ['heading' => 'Hello, from slot level 2!'],
'#slots' => [
'the_body' => [
'uuid-level-3' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-no-slots',
'#props' => ['heading' => 'Hello, from slot level 3!'],
],
'uuid-last-in-tree' => [
'#type' => 'component',
'#component' => 'xb_test_sdc:props-no-slots',
'#props' => ['heading' => 'Hello, from slot <LAST ONE>!'],
],
],
],
],
],
],
],
],
],
],
],
],
'expected HTML' => <<<HTML
<div data-component-id="xb_test_sdc:props-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, world!</h1>
<div class="component--props-slots--body">
<div data-component-id="xb_test_sdc:props-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, from slot level 1!</h1>
<div class="component--props-slots--body">
<div data-component-id="xb_test_sdc:props-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, from slot level 2!</h1>
<div class="component--props-slots--body">
<div data-component-id="xb_test_sdc:props-no-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, from slot level 3!</h1>
</div>
<div data-component-id="xb_test_sdc:props-no-slots" style="font-family: Helvetica, Arial, sans-serif; width: 100%; height: 100vh; background-color: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column; text-align: center; padding: 20px; box-sizing: border-box;">
<h1 style="font-size: 3em; margin: 0.5em 0; color: #333;">Hello, from slot &lt;LAST ONE&gt;!</h1>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
HTML,
];
}
}
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