I'm coming with 2 questions, I can't find the answer in the documentation:
Ok so I find a solution for recursive blocks. I'm overriding the HandleBlock repository behavior like:
CLICK ME
#### yes, even hidden code blocks!
<?php
namespace App\Repositories\Behaviors;
use A17\Twill\Models\Behaviors\HasMedias;
use A17\Twill\Repositories\BlockRepository;
trait HandleBlocks
{
public function hydrateHandleBlocks($object, $fields)
{
if ($this->shouldIgnoreFieldBeforeSave('blocks')) {
return;
}
$blocksCollection = collect();
$blocksFromFields = $this->getBlocks($object, $fields);
$blockRepository = app(BlockRepository::class);
$blocksFromFields->each(function ($block, $key) use ($blocksCollection, $blockRepository) {
$newBlock = $blockRepository->createForPreview($block);
$newBlock->id = $key + 1;
$childBlocksCollection = collect();
$block['blocks']->each(function ($childBlock) use ($newBlock, $blocksCollection, $blockRepository, $childBlocksCollection) {
$childBlock['parent_id'] = $newBlock->id;
$newChildBlock = $blockRepository->createForPreview($childBlock);
$blocksCollection->push($newChildBlock);
$childBlocksCollection->push($newChildBlock);
});
$newBlock->setRelation('children', $childBlocksCollection);
$blocksCollection->push($newBlock);
});
$object->setRelation('blocks', $blocksCollection);
return $object;
}
public function afterSaveHandleBlocks($object, $fields)
{
if ($this->shouldIgnoreFieldBeforeSave('blocks')) {
return;
}
$blockRepository = app(BlockRepository::class);
$blockRepository->bulkDelete($object->blocks()->pluck('id')->toArray());
$this->getBlocks($object, $fields)->each(function ($block) use ($object, $blockRepository) {
$blockCreated = $blockRepository->create($block);
$this->afterSaveHandleRecursiveBlocks($object, $blockRepository, $block, $blockCreated->id);
});
}
private function afterSaveHandleRecursiveBlocks($object, $blockRepository, $block, $parent_id) {
$block['blocks']->each(function ($childBlock) use ($object, $parent_id, $blockRepository) {
$childBlock['parent_id'] = $parent_id;
$blockCreated = $blockRepository->create($childBlock);
if ($childBlock['blocks']) {
$childBlockRepository = app(BlockRepository::class);
$this->afterSaveHandleRecursiveBlocks($object, $childBlockRepository, $childBlock, $blockCreated->id);
}
});
}
private function getBlocks($object, $fields)
{
$blocks = collect();
if (isset($fields['blocks']) && is_array($fields['blocks'])) {
foreach ($fields['blocks'] as $index => $block) {
$block = $this->buildBlock($block, $object);
$block['position'] = $index + 1;
$block['blocks'] = $this->getRecursiveBlocks($object, $block);
$blocks->push($block);
}
}
return $blocks;
}
public function getRecursiveBlocks($object, $block) {
$childBlocksList = collect();
foreach ($block['blocks'] as $childKey => $childBlocks) {
foreach ($childBlocks as $index => $childBlock) {
$childBlock = $this->buildBlock($childBlock, $object, true);
$childBlock['child_key'] = $childKey;
$childBlock['position'] = $index + 1;
if ($childBlock['blocks']) {
$childBlock['blocks'] = $this->getRecursiveBlocks($object, $childBlock);
}
$childBlocksList->push($childBlock);
}
}
return $childBlocksList;
}
private function buildBlock($block, $object, $repeater = false)
{
$block['blockable_id'] = $object->id;
$block['blockable_type'] = $object->getMorphClass();
return app(BlockRepository::class)->buildFromCmsArray($block, $repeater);
}
public function getFormFieldsHandleBlocks($object, $fields)
{
$fields['blocks'] = null;
if ($object->has('blocks')) {
$blocksConfig = config('twill.block_editor');
foreach ($object->blocks as $block) {
$isInRepeater = isset($block->parent_id);
$configKey = $isInRepeater ? 'repeaters' : 'blocks';
$blockTypeConfig = $blocksConfig[$configKey][$block->type] ?? null;
if (is_null($blockTypeConfig)) {
continue;
}
$blockItem = [
'id' => $block->id,
'type' => $blockTypeConfig['component'],
'title' => $blockTypeConfig['title'],
'attributes' => $blockTypeConfig['attributes'] ?? [],
];
if ($isInRepeater) {
$fields['blocksRepeaters']["blocks-{$block->parent_id}_{$block->child_key}"][] = $blockItem + [
'max' => $blockTypeConfig['max'],
'trigger' => $blockTypeConfig['trigger'],
];
} else {
$fields['blocks'][] = $blockItem + [
'icon' => $blockTypeConfig['icon'],
];
}
$fields['blocksFields'][] = collect($block['content'])->filter(function ($value, $key) {
return $key !== "browsers";
})->map(function ($value, $key) use ($block) {
return [
'name' => "blocks[$block->id][$key]",
'value' => $value,
];
})->filter()->values()->toArray();
$blockFormFields = app(BlockRepository::class)->getFormFields($block);
$medias = $blockFormFields['medias'];
if ($medias) {
$fields['blocksMedias'][] = collect($medias)->mapWithKeys(function ($value, $key) use ($block) {
return [
"blocks[$block->id][$key]" => $value,
];
})->filter()->toArray();
}
$files = $blockFormFields['files'];
if ($files) {
collect($files)->each(function ($rolesWithFiles, $locale) use (&$fields, $block) {
$fields['blocksFiles'][] = collect($rolesWithFiles)->mapWithKeys(function ($files, $role) use ($locale, $block) {
return [
"blocks[$block->id][$role][$locale]" => $files,
];
})->toArray();
});
}
if (isset($block['content']['browsers'])) {
$fields['blocksBrowsers'][] = $this->getBlockBrowsers($block);
}
}
if ($fields['blocksFields'] ?? false) {
$fields['blocksFields'] = call_user_func_array('array_merge', $fields['blocksFields'] ?? []);
}
if ($fields['blocksMedias'] ?? false) {
$fields['blocksMedias'] = call_user_func_array('array_merge', $fields['blocksMedias'] ?? []);
}
if ($fields['blocksFiles'] ?? false) {
$fields['blocksFiles'] = call_user_func_array('array_merge', $fields['blocksFiles'] ?? []);
}
if ($fields['blocksBrowsers'] ?? false) {
$fields['blocksBrowsers'] = call_user_func_array('array_merge', $fields['blocksBrowsers'] ?? []);
}
}
return $fields;
}
protected function getBlockBrowsers($block)
{
return collect($block['content']['browsers'])->mapWithKeys(function ($ids, $relation) use ($block) {
$relationRepository = $this->getModelRepository($relation);
$relatedItems = $relationRepository->get([], ['id' => $ids], [], -1);
$sortedRelatedItems = array_flip($ids);
foreach ($relatedItems as $item) {
$sortedRelatedItems[$item->id] = $item;
}
$items = collect(array_values($sortedRelatedItems))->filter(function ($value) {
return is_object($value);
})->map(function ($relatedElement) use ($relation) {
return [
'id' => $relatedElement->id,
'name' => $relatedElement->titleInBrowser ?? $relatedElement->title,
'edit' => moduleRoute($relation, config('twill.block_editor.browser_route_prefixes.' . $relation), 'edit', $relatedElement->id),
] + (classHasTrait($relatedElement, HasMedias::class) ? [
'thumbnail' => $relatedElement->defaultCmsImage(['w' => 100, 'h' => 100]),
] : []);
})->toArray();
return [
"blocks[$block->id][$relation]" => $items,
];
})->filter()->toArray();
}
}
Maybe it can deserve a PR?
Hey @bastienrobert,
This is an interesting approach.
Do you have a specific use case for implementing this change?
I'm also curious to know if you though about how this would affect the renderBlocks() helper or how should the renderBlocks be changed to contemplate this feature.
Best.
Hi @sauron, I'm working on renderBlocks() method (I absolutely need it to order my embedded blocks because I need to render a REST API) and I think I could share it here in a few days.
I was working on this for a complex menu for an e-shop website I'm working on for a customer, which needs embedded blocks, something like:
Finally, it's the only thing I had to edit. The front-end is already working well 馃憣
Here is how I finally got my recursive blocks to send them as a JSON response from an external API controller:
protected function getOrderedBlocks($blocks, $parent_id = null) {
return array_values(json_decode(json_encode($blocks->where('parent_id', $parent_id)->map(function ($block) use($blocks) {
$children = array_values(json_decode(json_encode($blocks->where('parent_id', $block->id)), true));
foreach ($children as &$child) {
$child['children'] = $this->getOrderedBlocks($blocks, $child['id']);
}
$block->children = $children;
return $block;
})), true));
}
Hi @bastienrobert,
Cool use case! And interesting way of building a menu too. I would tend to generate menus out of the actual content but I can see how what you're building can be useful. Do you plan on attaching those menus items to actual content records? Maybe through a browser field?
Regarding the recursive repeaters, wow! I'm curious to see the UI it produces with your setup. We didn't consider going down more levels in design and I'm actually surprised to hear that the frontend seem to already work with more levels down!
As for supporting blocks in settings, I'm definitely not against it and it should be fairly straightforward to implement.
Yep, already did that! My menu links (in the bottom of my tree) are just two fields: a label (translated) and a browser field to link pages.
The front-end looks like this (for me, it rocks 馃槏):

Cool news!
"Embedded blocks" or "nested repeaters" are now supported thanks to #521. Keeping this open and renaming to Blocks on settings only as this is definitely something that should be possible.
Most helpful comment
Yep, already did that! My menu links (in the bottom of my tree) are just two fields: a label (translated) and a browser field to link pages.
The front-end looks like this (for me, it rocks 馃槏):
Cool news!