Nested Modules

Out of the box, Twill supports 2 kinds of nested modules: self-nested and parent-child.

Self-nested modules

Self-nested modules allow items to be nested within other items of the same module (e.g. Pages can contain other Pages):

self-nested module

Creating self-nested modules

You can enable nesting when creating a new module with the --hasNesting or -N option:

1php artisan twill:make:module -N pages

This will prefill some options and methods in your module's controller and use the supporting traits on your model and repository.

Working with self-nested items

A few accessors and methods are available to work with nested item slugs:

1// Get the combined slug for all ancestors of an item in the current locale:
2$slug = $item->ancestorsSlug;
3 
4// for a specific locale:
5$slug = $item->getAncestorsSlug($lang);
6 
7// Get the combined slug for an item including all ancestors:
8$slug = $item->nestedSlug;
9 
10// for a specific locale:
11$slug = $item->getNestedSlug($lang);

To include all ancestor slugs in the permalink of an item in the CMS, you can dynamically set the $permalinkBase property from the form() method of your module controller:

1class PageController extends NestedModuleController
2{
3 //...
4 
5 protected function form(?int $id, TwillModelContract $item = null): array
6 {
7 $item = $this->repository->getById($id, $this->formWith, $this->formWithCount);
8 
9 $this->permalinkBase = $item->ancestorsSlug;
10 
11 return parent::form($id, $item);
12 }
13}

To implement routing for nested items, you can combine the forNestedSlug() method from HandleNesting with a wildcard route parameter:

1// file: routes/web.php
2 
3Route::get('{slug}', function ($slug) {
4 $page = app(PageRepository::class)->forNestedSlug($slug);
5 
6 abort_unless($page, 404);
7 
8 return view('site.page', ['page' => $page]);
9})->where('slug', '.*');

For more information on how to work with nested items in your application, you can refer to the laravel-nestedset package documentation.

Setting a maximum nested depth

You can also define the maximum depth allowed for the module changing the following:

1protected $nestedItemsDepth = 1;

Note: a depth of 1 means parent and child.

Working with browser fields

By default only a parent item will be visible to the browser field. If you want to show child items when browsing for the module you can set $showOnlyParentItemsInBrowsers to false:

1protected $showOnlyParentItemsInBrowsers = false; // default is true

Parent-child modules

Parent-child modules are 2 distinct modules, where items of the child module are attached to items of the parent module (e.g. Issues can contain Articles):

parent-child modules

Items of the child module can't be created independently.

Creating parent-child modules

We'll use the slug and position features in this example but you can customize as needed:

1php artisan twill:make:module issues -SP
2php artisan twill:make:module issueArticles -SP --parentModel=Issue

Add the issue_id foreign key to the child module's migration:

1public function up()
2{
3 Schema::create('issue_articles', function (Blueprint $table) {
4 // ...
5 $table->foreignIdFor(Issue::class);
6 });
7 
8 // ...
9}

Run the migrations:

1php artisan migrate

Update the child model. Add the issue_id fillable and the relationship to the parent model:

1class IssueArticle extends Model implements Sortable
2{
3 // ...
4 
5 protected $fillable = [
6 // ...
7 'issue_id',
8 ];
9 
10 public function issue()
11 {
12 return $this->belongsTo(Issue::class);
13 }
14}

Update the parent model. Add the relationship to the child model:

1class Issue extends Model implements Sortable
2{
3 // ...
4 
5 public function articles()
6 {
7 return $this->hasMany(IssueArticle::class);
8 }
9}

Update the child controller. Set the $moduleName and $modelName properties, then override the getParentModuleForeignKey() method:

1use A17\Twill\Services\Breadcrumbs\NestedBreadcrumbs;
2 
3class IssueArticleController extends BaseModuleController
4{
5 protected $moduleName = 'issues.articles';
6 protected $modelName = 'IssueArticle';
7 
8 protected function setUpController(): void
9 {
10 if (request('issue')) {
11 $this->setBreadcrumbs(
12 NestedBreadcrumbs::make()
13 ->forParent(
14 parentModule: 'issues',
15 module: $this->moduleName,
16 activeParentId: request('issue'),
17 repository: \App\Repositories\IssueRepository::class
18 )
19 ->label('Article')
20 );
21 }
22 }
23}

Update the parent controller. Set the $indexColumns property to include a new Articles column. This will be a link to the child module items, for each parent.

1 
2use A17\Twill\Services\Listings\Columns\NestedData;
3use A17\Twill\Services\Listings\TableColumns;
4 
5class IssueController extends BaseModuleController
6{
7 protected $moduleName = 'issues';
8 
9 protected function additionalIndexTableColumns(): TableColumns
10 {
11 $table = parent::additionalIndexTableColumns();
12 
13 $table->add(
14 NestedData::make()->field('articles')
15 );
16 
17 return $table;
18 }
19}

Add both modules to routes/twill.php:

1<?php
2 
3use A17\Twill\Facades\TwillRoutes;
4 
5TwillRoutes::module('issues');
6TwillRoutes::module('issues.articles');

Add the parent module to AppServiceProvider.php:

1<?php
2 
3namespace App\Providers;
4 
5use A17\Twill\Facades\TwillNavigation;
6use A17\Twill\View\Components\Navigation\NavigationLink;
7use Illuminate\Support\ServiceProvider;
8 
9class AppServiceProvider extends ServiceProvider
10{
11 public function boot(): void
12 {
13 TwillNavigation::addLink(
14 NavigationLink::make()->forModule('issues')
15 );
16 }
17}

Only when using blade forms:

Rename and move the articles/ views folder inside of the parent issues/ folder:

1resources/views/twill/
2└── issues
3 ├── articles
4 │ └── form.blade.php
5 └── form.blade.php
6...

Breadcrumbs

Breadcrumbs are added via the $this->setBreadcrumbs method, you can remove that line if you wish to not include the breadcrumbs.

child module index nested child form