Twill: Blocks on settings

Created on 14 Mar 2019  路  7Comments  路  Source: area17/twill

I'm coming with 2 questions, I can't find the answer in the documentation:

  • Is there any way to add blocks and/or repeatable fields on settings?
  • What about repeatable blocks inside repeatable parents blocks ?
enhancement question

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 馃槏):

image

Cool news!

All 7 comments

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:

  • Menu category (top level category)

    • Menu column (because each category can have columns)

    • Menu item (in a column, I can have multiple items)



      • Menu link (finally)



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 馃槏):

image

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dafyd picture dafyd  路  3Comments

milewski picture milewski  路  4Comments

madsem picture madsem  路  4Comments

noxify picture noxify  路  4Comments

stevanpavlovic picture stevanpavlovic  路  7Comments