Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sjardim/063e1f3526da697b7d77156c4e63cb9b to your computer and use it in GitHub Desktop.
Save sjardim/063e1f3526da697b7d77156c4e63cb9b to your computer and use it in GitHub Desktop.
Block editor templates

Prefill a block editor from a selection of templates

Objectives:

  • Create a new module with a template field
  • Prefill the block editor for new items according to the selected template

Requirements:

Versions used at the time of writing:

Version
PHP 8.0
Laravel 8.61
Twill 2.5.2

Create the new module

php artisan twill:make:module articles -B

We'll make sure to enable blocks on the module, everything else is optional. In this example, we won't be using translations, but they can be added with minor changes.

Update the migration

We'll add the template field to the generated migration:

// update file: database/migrations/xxxx_xx_xx_xxxxxx_create_articles_tables.php

    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            // ...

            $table->text('template')->nullable();
        });
    }

Then, we'll run the migrations:

php artisan migrate

and add the module to our routes/admin.php and config/twill-navigation.php.

Update the model

In this example, we imagine 3 templates that our authors can choose from:

  • Full Article — an original article on our blog
  • Linked Article — a short article to summarize and share interesting articles from other blogs
  • Empty — a blank canvas

We'll start by adding our new field to the fillables:

// update file: app/Models/Article.php

class Article extends Model
{
    protected $fillable = [
        // ...
        
        'template',
    ];
}

Then, we'll define some constants for our template options:

// update file: app/Models/Article.php

    const DEFAULT_TEMPLATE = 'full_article';

    const AVAILABLE_TEMPLATES = [
        [
            'value' => 'full_article',
            'label' => 'Full Article',
        ],
        [
            'value' => 'linked_article',
            'label' => 'Linked Article',
        ],
        [
            'value' => 'empty',
            'label' => 'Empty',
        ],
    ];

We'll add an attribute accessor to get the template name for the currently selected template value:

// update file: app/Models/Article.php

    public function getTemplateLabelAttribute()
    {
        $template = collect(static::AVAILABLE_TEMPLATES)
            ->firstWhere('value', $this->template);

        return $template['label'] ?? '';
    }

This will be useful in our create.blade.php view below.

Add the template field to the create modal

When running php artisan twill:make:module, we get a form.blade.php to define the main form for our module. In addition, it's also possible to redefine the fields that are displayed in the create modal, before the form:

01-create-modal

We'll copy Twill's built-in view from vendor/area17/twill/views/partials/create.blade.php into our project, then add our template field:

// create file: resources/views/admin/articles/create.blade.php

@formField('input', [
    'name' => $titleFormKey ?? 'title',
    'label' => $titleFormKey === 'title' ? 
        twillTrans('twill::lang.modal.title-field') : ucfirst($titleFormKey),
    'required' => true,
    'onChange' => 'formatPermalink'
])

@if ($item->template ?? false)
    {{-- 
        On update, we show the selected template in a disabled field.
        For simplicity, templates cannot be modified once an item has been created.
    --}}

    @formField('input', [
        'name' => 'template_label',
        'label' => 'Template',
        'disabled' => true,
    ])
@else
    {{-- 
        On create, we show a select field with all possible templates.
    --}}

    @formField('select', [
        'name' => 'template',
        'label' => 'Template',
        'default' => \App\Models\Article::DEFAULT_TEMPLATE,
        'options' => \App\Models\Article::AVAILABLE_TEMPLATES,
    ])
@endif

@if ($permalink ?? true)
    @formField('input', [
        'name' => 'slug',
        'label' => twillTrans('twill::lang.modal.permalink-field'),
        'translated' => true, //Twill 2.6.0 return [Object objet] on slug field without this as true
        'ref' => 'permalink',
        'prefix' => $permalinkPrefix ?? ''
    ])
@endif

Create some blocks

php artisan twill:make:block article-header
php artisan twill:make:block article-paragraph
php artisan twill:make:block article-references
php artisan twill:make:block linked-article

You'll find some example views for each block at the end of this recipe.

Define the block selection for each template

We'll update our AVAILABLE_TEMPLATES with the list of blocks that should be prefilled for each template, then add the AVAILABLE_BLOCKS for our form:

// update file: app/Models/Article.php

    const DEFAULT_TEMPLATE = 'full_article';

    const AVAILABLE_TEMPLATES = [
        [
            'value' => 'full_article',
            'label' => 'Full Article',
            'block_selection' => ['article-header', 'article-paragraph', 'article-references'],
        ],
        [
            'value' => 'linked_article',
            'label' => 'Linked Article',
            'block_selection' => ['article-header', 'linked-article'],
        ],
        [
            'value' => 'empty',
            'label' => 'Empty',
            'block_selection' => [],
        ],
    ];

    const AVAILABLE_BLOCKS = [
        'article-header', 'article-paragraph', 'article-references', 'linked-article'
    ];

Then, we'll add the block editor field to our form:

// update file: resources/views/admin/articles/form.blade.php

    @formField('block_editor', [
        'blocks' => \App\Models\Article::AVAILABLE_BLOCKS,
    ])

Prefill the blocks on create

With this, all that's needed is to initialize the block editor from the selected template. We'll update our model to add the prefill operation:

// update file: app/Models/Article.php

    public function getTemplateBlockSelectionAttribute()
    {
        $template = collect(static::AVAILABLE_TEMPLATES)
            ->firstWhere('value', $this->template);

        return $template['block_selection'] ?? [];
    }

    public function prefillBlockSelection()
    {
        $i = 1;

        foreach ($this->template_block_selection as $blockType) {
            app(\A17\Twill\Repositories\BlockRepository)->create([
                'blockable_id' => $this->id,
                'blockable_type' => static::class,
                'position' => $i++,
                'content' => '{}',
                'type' => $blockType,
            ]);
        }
    }

Then, we'll hook into the repository's afterSave():

// update file: app/Repositories/ArticleRepository.php

class ArticleRepository extends ModuleRepository
{
    // ...

    public function afterSave($object, $fields)
    {
        parent::afterSave($object, $fields);

        if ($object->wasRecentlyCreated) {
            $object->prefillBlockSelection();
        }
    }
}

The check on $object->wasRecentlyCreated ensures the prefill operation will only run when the record is first created.

Finished result

And there we have it — a templating mechanism for our block editor:

02-edit-form



Thanks for reading and have fun :)
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateArticlesTables extends Migration
{
public function up()
{
Schema::create('articles', function (Blueprint $table) {
createDefaultTableFields($table);
$table->string('title', 200)->nullable();
$table->text('description')->nullable();
$table->integer('position')->unsigned()->nullable();
$table->text('template')->nullable();
});
Schema::create('article_slugs', function (Blueprint $table) {
createDefaultSlugsTableFields($table, 'article');
});
Schema::create('article_revisions', function (Blueprint $table) {
createDefaultRevisionsTableFields($table, 'article');
});
}
public function down()
{
Schema::dropIfExists('article_revisions');
Schema::dropIfExists('article_slugs');
Schema::dropIfExists('articles');
}
}
<?php
namespace App\Models;
use A17\Twill\Models\Behaviors\HasBlocks;
use A17\Twill\Models\Behaviors\HasSlug;
use A17\Twill\Models\Behaviors\HasMedias;
use A17\Twill\Models\Behaviors\HasFiles;
use A17\Twill\Models\Behaviors\HasRevisions;
use A17\Twill\Models\Behaviors\HasPosition;
use A17\Twill\Models\Behaviors\Sortable;
use A17\Twill\Models\Model;
use A17\Twill\Repositories\BlockRepository;
class Article extends Model implements Sortable
{
use HasBlocks, HasSlug, HasMedias, HasFiles, HasRevisions, HasPosition;
const DEFAULT_TEMPLATE = 'full_article';
const AVAILABLE_TEMPLATES = [
[
'value' => 'full_article',
'label' => 'Full Article',
'block_selection' => ['article-header', 'article-paragraph', 'article-references'],
],
[
'value' => 'linked_article',
'label' => 'Linked Article',
'block_selection' => ['article-header', 'linked-article'],
],
[
'value' => 'empty',
'label' => 'Empty',
'block_selection' => [],
],
];
const AVAILABLE_BLOCKS = ['article-header', 'article-paragraph', 'article-references', 'linked-article'];
protected $fillable = [
'published',
'title',
'description',
'position',
'template',
];
public $slugAttributes = [
'title',
];
public function getTemplateLabelAttribute()
{
$template = collect(static::AVAILABLE_TEMPLATES)->firstWhere('value', $this->template);
return $template['label'] ?? '';
}
public function getTemplateBlockSelectionAttribute()
{
$template = collect(static::AVAILABLE_TEMPLATES)->firstWhere('value', $this->template);
return $template['block_selection'] ?? [];
}
public function prefillBlockSelection()
{
$i = 1;
foreach ($this->template_block_selection as $blockType) {
app(BlockRepository::class)->create([
'blockable_id' => $this->id,
'blockable_type' => static::class,
'position' => $i++,
'content' => '{}',
'type' => $blockType,
]);
}
}
}
<?php
namespace App\Repositories;
use A17\Twill\Repositories\Behaviors\HandleBlocks;
use A17\Twill\Repositories\Behaviors\HandleSlugs;
use A17\Twill\Repositories\Behaviors\HandleMedias;
use A17\Twill\Repositories\Behaviors\HandleFiles;
use A17\Twill\Repositories\Behaviors\HandleRevisions;
use A17\Twill\Repositories\ModuleRepository;
use App\Models\Article;
class ArticleRepository extends ModuleRepository
{
use HandleBlocks, HandleSlugs, HandleMedias, HandleFiles, HandleRevisions;
public function __construct(Article $model)
{
$this->model = $model;
}
public function afterSave($object, $fields)
{
parent::afterSave($object, $fields);
if ($object->wasRecentlyCreated) {
$object->prefillBlockSelection();
}
}
}
@formField('input', [
'name' => $titleFormKey ?? 'title',
'label' => $titleFormKey === 'title' ? twillTrans('twill::lang.modal.title-field') : ucfirst($titleFormKey),
'required' => true,
'onChange' => 'formatPermalink'
])
@if ($item->template ?? false)
@formField('input', [
'name' => 'template_label',
'label' => 'Template',
'disabled' => true,
])
@else
@formField('select', [
'name' => 'template',
'label' => 'Template',
'default' => \App\Models\Article::DEFAULT_TEMPLATE,
'options' => \App\Models\Article::AVAILABLE_TEMPLATES,
])
@endif
@if ($permalink ?? true)
@formField('input', [
'name' => 'slug',
'label' => twillTrans('twill::lang.modal.permalink-field'),
'ref' => 'permalink',
'prefix' => $permalinkPrefix ?? ''
])
@endif
@extends('twill::layouts.form')
@section('contentFields')
@formField('input', [
'name' => 'description',
'label' => 'Description',
'maxlength' => 100
])
@formField('block_editor', [
'blocks' => \App\Models\Article::AVAILABLE_BLOCKS,
])
@stop
@twillBlockTitle('Article Header')
@twillBlockIcon('text')
@twillBlockGroup('app')
@formField('input', [
'name' => 'subtitle',
'label' => 'Subtitle',
])
@formField('input', [
'name' => 'author',
'label' => 'Author',
])
@formField('input', [
'name' => 'reading_time',
'label' => 'Estimated Reading Time',
])
@twillBlockTitle('Article Paragraph')
@twillBlockIcon('text')
@twillBlockGroup('app')
@formField('wysiwyg', [
'name' => 'text',
'label' => 'Text',
'placeholder' => 'Text',
'toolbarOptions' => ['bold', 'italic', 'link', 'clean'],
])
@twillBlockTitle('Article References')
@twillBlockIcon('text')
@twillBlockGroup('app')
@formField('wysiwyg', [
'name' => 'text',
'label' => 'Text',
'placeholder' => 'Text',
'toolbarOptions' => ['bold', 'italic', 'link', 'clean'],
])
@twillBlockTitle('Linked Article')
@twillBlockIcon('text')
@twillBlockGroup('app')
@formField('input', [
'name' => 'title',
'label' => 'Article Title',
])
@formField('input', [
'name' => 'description',
'label' => 'Article Link',
'type' => 'textarea',
'rows' => 4,
])
@formField('input', [
'name' => 'url',
'label' => 'Article URL',
])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment