Prefilling 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

Versions used at the time of writing:

Version
PHP 8.0
Laravel 8.x

Create the new module

1php 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:

File:

database/migrations/2021_09_19_131244_create_articles_tables.php

1<?php
2 
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5 
6return new class extends Migration
7{
8 public function up()
9 {
10 Schema::create('articles', function (Blueprint $table) {
11 createDefaultTableFields($table);
12 
13 $table->string('title', 200)->nullable();
14 $table->text('description')->nullable();
15 $table->integer('position')->unsigned()->nullable();
16 $table->text('template')->nullable();
17 });
18 
19 Schema::create('article_slugs', function (Blueprint $table) {
20 createDefaultSlugsTableFields($table, 'article');
21 });
22 
23 Schema::create('article_revisions', function (Blueprint $table) {
24 createDefaultRevisionsTableFields($table, 'article');
25 });
26 }
27 
28 public function down()
29 {
30 Schema::dropIfExists('article_revisions');
31 Schema::dropIfExists('article_slugs');
32 Schema::dropIfExists('articles');
33 }
34};

Then, we'll run the migrations:

1php 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:

File:

app/Models/Article.php

1protected $fillable = [
2 'published',
3 'title',
4 'description',
5 'position',
6 'template',
7];

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

File:

app/Models/Article.php

1public const DEFAULT_TEMPLATE = 'full_article';
2 
3public const AVAILABLE_TEMPLATES = [
4 [
5 'value' => 'full_article',
6 'label' => 'Full Article',
7 'block_selection' => ['article-header', 'article-paragraph', 'article-references'],
8 ],
9 [
10 'value' => 'linked_article',
11 'label' => 'Linked Article',
12 'block_selection' => ['article-header', 'linked-article'],
13 ],
14 [
15 'value' => 'empty',
16 'label' => 'Empty',
17 'block_selection' => [],
18 ],
19];

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

File:

app/Models/Article.php

1public function getTemplateLabelAttribute()
2{
3 $template = collect(static::AVAILABLE_TEMPLATES)->firstWhere('value', $this->template);
4 
5 return $template['label'] ?? '';
6}

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:

File:

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

1<x-twill::input
2 :name="$titleFormKey ?? 'title'"
3 :label="$titleFormKey === 'title' ? twillTrans('twill::lang.modal.title-field') : ucfirst($titleFormKey)"
4 :required="true"
5 on-change="formatPermalink"
6/>
7 
8@if ($item->template ?? false)
9 {{--
10 On update, we show the selected template in a disabled field.
11 For simplicity, templates cannot be modified once an item has been created.
12 --}}
13 <x-twill::input
14 name="template_label"
15 label="Template"
16 :disabled="true"
17 />
18@else
19 {{--
20 On create, we show a select field with all possible templates.
21 --}}
22 <x-twill::select
23 name="template"
24 label="Template"
25 :default="\App\Models\Article::DEFAULT_TEMPLATE"
26 :options="\App\Models\Article::AVAILABLE_TEMPLATES"
27 />
28@endif
29 
30@if ($permalink ?? true)
31 <x-twill::input
32 name="slug"
33 :label="twillTrans('twill::lang.modal.permalink-field')"
34 ref="permalink"
35 :prefix="$permalinkPrefix ?? ''"
36 />
37@endif

Create some blocks

1php artisan twill:make:block article-header
2php artisan twill:make:block article-paragraph
3php artisan twill:make:block article-references
4php artisan twill:make:block linked-article

File:

resources/views/twill/blocks/article-header.blade.php

1@twillBlockTitle('Article Header')
2@twillBlockIcon('text')
3@twillBlockGroup('app')
4 
5<x-twill::input
6 name="subtitle"
7 label="Subtitle"
8/>
9 
10<x-twill::input
11 name="author"
12 label="Author"
13/>
14 
15<x-twill::input
16 name="reading_time"
17 label="Estimated Reading Time"
18/>

File:

resources/views/twill/blocks/article-paragraph.blade.php

1@twillBlockTitle('Article Paragraph')
2@twillBlockIcon('text')
3@twillBlockGroup('app')
4 
5<x-twill::wysiwyg
6 name="text"
7 label="Text"
8 placeholder="Text"
9 :toolbar-options="['bold', 'italic', 'link', 'clean']"
10/>

File:

resources/views/twill/blocks/article-references.blade.php

1@twillBlockTitle('Article References')
2@twillBlockIcon('text')
3@twillBlockGroup('app')
4 
5<x-twill::wysiwyg
6 name="text"
7 label="Text"
8 placeholder="Text"
9 :toolbar-options="['bold', 'italic', 'link', 'clean']"
10/>

File:

resources/views/twill/blocks/linked-post.blade.php

1@twillBlockTitle('Linked Article')
2@twillBlockIcon('text')
3@twillBlockGroup('app')
4 
5<x-twill::input
6 name="title"
7 label="Article title"
8/>
9 
10<x-twill::input
11 name="description"
12 label="Article link"
13 type="textarea"
14 :rows="4"
15/>
16 
17<x-twill::input
18 name="url"
19 label="Article URL"
20/>

Add the editor to our form

We'll add the block editor field to our form:

File:

resources/views/admin/articles/form.blade.php

1@extends('twill::layouts.form')
2 
3@section('contentFields')
4 <x-twill::input
5 name="description"
6 label="Description"
7 :maxlength="100"
8 />
9 
10 <x-twill::block-editor
11 :blocks="\App\Models\Article::AVAILABLE_BLOCKS"
12 />
13@stop

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:

File:

app/Models/Article.php

1<?php
2 
3namespace App\Models;
4 
5use A17\Twill\Models\Behaviors\HasBlocks;...
6use A17\Twill\Models\Behaviors\HasFiles;
7use A17\Twill\Models\Behaviors\HasMedias;
8use A17\Twill\Models\Behaviors\HasPosition;
9use A17\Twill\Models\Behaviors\HasRevisions;
10use A17\Twill\Models\Behaviors\HasSlug;
11use A17\Twill\Models\Behaviors\Sortable;
12use A17\Twill\Models\Model;
13use A17\Twill\Repositories\BlockRepository;
14 
15class Article extends Model implements Sortable
16{
17 use HasBlocks;...
18 use HasSlug;
19 use HasMedias;
20 use HasFiles;
21 use HasRevisions;
22 use HasPosition;
23 
24 // #region constants
25 public const DEFAULT_TEMPLATE = 'full_article';
26 
27 public const AVAILABLE_TEMPLATES = [
28 [
29 'value' => 'full_article',
30 'label' => 'Full Article',
31 'block_selection' => ['article-header', 'article-paragraph', 'article-references'],
32 ],
33 [
34 'value' => 'linked_article',
35 'label' => 'Linked Article',
36 'block_selection' => ['article-header', 'linked-article'],
37 ],
38 [
39 'value' => 'empty',
40 'label' => 'Empty',
41 'block_selection' => [],
42 ],
43 ];
44 
45 // #endregion constants
46 
47 public const AVAILABLE_BLOCKS = ['article-header', 'article-paragraph', 'article-references', 'linked-article'];
48 
49 // #region fillable
50 protected $fillable = [...
51 'published',
52 'title',
53 'description',
54 'position',
55 'template',
56 ];
57 
58 // #endregion fillable
59 
60 public $slugAttributes = [...
61 'title',
62 ];
63 
64 // #region accessor
65 public function getTemplateLabelAttribute()...
66 {
67 $template = collect(static::AVAILABLE_TEMPLATES)->firstWhere('value', $this->template);
68 
69 return $template['label'] ?? '';
70 }
71 
72 // #endregion accessor
73 
74 // #region prefill
75 public function getTemplateBlockSelectionAttribute()
76 {
77 $template = collect(static::AVAILABLE_TEMPLATES)->firstWhere('value', $this->template);
78 
79 return $template['block_selection'] ?? [];
80 }
81 
82 public function prefillBlockSelection()
83 {
84 $i = 1;
85 
86 foreach ($this->template_block_selection as $blockType) {
87 app(BlockRepository::class)->create([
88 'blockable_id' => $this->id,
89 'blockable_type' => static::class,
90 'position' => $i++,
91 'content' => '{}',
92 'type' => $blockType,
93 ]);
94 }
95 }
96 
97 // #endregion prefill
98}

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

File:

app/Repositories/ArticleRepository.php

1<?php
2 
3namespace App\Repositories;
4 
5use A17\Twill\Repositories\Behaviors\HandleBlocks;...
6use A17\Twill\Repositories\Behaviors\HandleSlugs;
7use A17\Twill\Repositories\Behaviors\HandleMedias;
8use A17\Twill\Repositories\Behaviors\HandleFiles;
9use A17\Twill\Repositories\Behaviors\HandleRevisions;
10use A17\Twill\Repositories\ModuleRepository;
11use App\Models\Article;
12 
13class ArticleRepository extends ModuleRepository
14{
15 use HandleBlocks, HandleSlugs, HandleMedias, HandleFiles, HandleRevisions;
16 
17 public function __construct(Article $model)...
18 {
19 $this->model = $model;
20 }
21 
22 public function afterSave($model, $fields)
23 {
24 parent::afterSave($model, $fields);
25 
26 if ($model->wasRecentlyCreated) {
27 $model->prefillBlockSelection();
28 }
29 }
30}

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