menu
announcement

Spectrum will become read-only on August 10, 2021. Learn more about the decision in our official announcement.

Twill

Rapidly create a custom admin console that content publishers will love. Twill is an open source CMS toolkit for Laravel, crafted by AREA 17.

Channels
# All channels
view-forward
# General
view-forward
# Announcements
view-forward
# Feature requests
view-forward
# Help
view-forward
# Jobs
view-forward
# Product vision
view-forward
# Resources
view-forward
# Show and tell
view-forward
# Tips and tricks
view-forward
Team

Step by step II- Creating a Twill app.

April 1, 2019 at 5:29pm

Step by step II- Creating a Twill app.

April 1, 2019 at 5:29pm (Edited 2 years ago)
The first part of this turorial is available here

Step by step - Creating a Twill app.

thumbsup
27
message-simple
99
All the code written for this tutorial is available on github https://github.com/sauron/my-twill-blog

Associations and Browsers

For this practice, I'll assume that an Article can have an Invitee writer. As we don't want to type Invitee's name every time, we need to create a Lookup Table for the Invitees.
The Invitee will have a Title and a Bio which can be translates(-T), may have his own page for which we need a slug(-S), and it will have a profile picture(-M)
php artisan twill:module invitees -TSM
We adjust the migration.
class CreateInviteesTables extends Migration
{
public function up()
{
Schema::create('invitees', function (Blueprint $table) {
createDefaultTableFields($table);
});
Schema::create('invitee_translations', function (Blueprint $table) {
createDefaultTranslationsTableFields($table, 'invitee');
$table->string('title')->nullable();
$table->text('bio')->nullable();
});
Schema::create('invitee_slugs', function (Blueprint $table) {
createDefaultSlugsTableFields($table, 'invitee');
});
}
public function down()
{
Schema::dropIfExists('invitee_translations');
Schema::dropIfExists('invitee_slugs');
Schema::dropIfExists('invitees');
}
}
Let's run the migration.
php artisan migrate
And then adjust the Model.
<?php
namespace App\Models;
use A17\Twill\Models\Behaviors\HasTranslation;
use A17\Twill\Models\Behaviors\HasSlug;
use A17\Twill\Models\Behaviors\HasMedias;
use A17\Twill\Models\Model;
class Invitee extends Model
{
use HasTranslation, HasSlug, HasMedias;
protected $fillable = [
'published',
];
public $translatedAttributes = [
'title',
'bio',
];
public $slugAttributes = [
'title',
];
public $checkboxes = [
'published'
];
public $mediasParams = [
'profile' => [
'default' => [
[
'name' => 'landscape',
'ratio' => 1 / 1,
],
],
],
];
}
in this case, we need to pay special attention to the InviteeTranslation model.
class InviteeTranslation extends Model
{
protected $fillable = [
'title',
'bio',
'active',
'locale',
];
}
With the DB structure in place we now want to access via our CMS, hence we add it to the CMS menu by adding the routes.
Route::module('invitees');
And enabling the menu option in the navigation config\twill-navigation.php
'invitees' => [
'title' => 'Invitees',
'module' => true
]
Last but not least, the form
@extends('twill::layouts.form')
@section('contentFields')
@formField('input', [
'name' => 'bio',
'label' => 'BIO',
'type' => 'textarea',
'translated' => true,
'maxlength' => 100
])
@formField('medias',[
'name' => 'profile',
'label' => 'Profile picture',
])
@stop
Now we can enter as many Invitees as we want
Screen Shot 2019-03-31 at 18.04.02.pngScreen Shot 2019-03-31 at 18.08.05.png
The next step is to be able to select the invitees to the Articles, for that let's add a browser for the invitees to the article's form Up to 2 invitees in this case.
@formField('browser', [
'moduleName' => 'invitees',
'name' => 'invitees',
'label' => 'Invitees',
'max' => 2
])
With that in-place we can add attach invitees to an article.
Screen Shot 2019-03-31 at 18.57.50.png
But if we save it, we will not receive any error and nothing will be stored. This means it it time to create the association table with php artisan make:migration CreateArticleInviteeTable The command will create an empty migration and we will add what we need.
public function up()
{
Schema::create('article_invitee', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
$table->integer('position')->unsigned();
createDefaultRelationshipTableFields($table, 'article', 'invitee');
});
}
Migration time...
php artisan migrate
With the DB to support we need to store the selected Invitees after saving the Article. Lets make sure that Articles knows about their invitees by adding to the Article Model, this:
public function invitees()
{
return $this->belongsToMany(\App\Models\Invitee::class);
}
On the ArticleRepository we add the afterSave function.
public function afterSave($object, $fields)
{
$this->updateBrowser($object, $fields, 'invitees');
parent::afterSave($object, $fields);
}
Also, for displaying the selected records we need to update the browser Fields that are displayed on the Form.
public function getFormFields($object)
{
$fields = parent::getFormFields($object);
$fields['browsers']['invitees'] = $this->getFormFieldsForBrowser($object, 'invitees');
return $fields;
}
We are all set for adding and saving the associated data.
Screen Shot 2019-03-31 at 19.55.07.png

Visual Block Editor

Now we have everything we need to build an Article we may want to enter the flexible content in the Block Editor directly.
Screen Shot 2019-03-31 at 20.16.51.png
If we click on any of the buttons we will receive an error
Screen Shot 2019-03-31 at 20.25.42.png
That cause of the error is we don't have the Blocks built for the FE. Time to follow the instructions again.
mkdir resources/views/layouts
touch resources/views/layouts/block.blade.php
touch resources/views/layouts/app.blade.php
The app.blade.php file will contain the basic HTML and the yield for the content.
<!DOCTYPE html>
<html dir="ltr" lang="en-US">
<head>
<meta charset="utf-8" />
</head>
<body>
@yield('content')
</body>
</html>
We will extend the block's layout with the app layout.
@extends('layouts.app')
Now the error is gone, but if we want to see the actual blocks, let's add some views.
Let's create the quote.blade.php inside the resources/views/site/blocks/
<div class="quote">
<p>{{ $block->translatedinput('quote') }}</p>
</div>
For the paragraph.blade.php
<div class="paragraph">
{!! $block->translatedinput('paragraph') !!}
</div>
There is one thing to consider when working with images For the image_with_text.blade.php
<div class="image_with_text">
<div>
{{ $block->input('text') }}
</div>
<div>
{{ $block->input('text') }}
</div>
</div>
And the gallery.blade.php
@php( $images = $block->images('gallery', 'default') )
@if( $images && sizeof($images) )
<div class="gallery">
<ul class="slides">
@foreach( $images as $item )
<li class="glide__slide">
<img src="{{ $item }}" alt="">
</li>
@endforeach
</ul>
</div>
@endif
Everything ready for using the Block Editor.
Screen Shot 2019-03-31 at 22.33.47.pngScreen Shot 2019-03-31 at 22.34.35.png

Preview

Now we have the basic FE for the Block Editor. We can create the preview for the Articles.
First, let's click on the Preview link to see the error.
Screen Shot 2019-03-31 at 23.26.24.png
I'm not gonna create the resources/views/site/article.blade.php file. I'll add a new folder resources/views/site/articles and in there I'll create my view. So:
mkdir resources/views/site/articles
touch resources/views/site/articles/show.blade.php
In the show view, I'll add a simple code for this displaying the Article.
<div class='hero'>
@if( $item->hasImage('hero_image'))
<img src="{{ $item->image('hero_image', 'default') }}">
@endif
<h1>{{ $item->title }}</h1>
<p>{{ $item->description }}</p>
</div>
We are in good shape to render the Blocks in the show view.
<div class='hero'>
@if( $item->hasImage('hero_image'))
<img src="{{ $item->image('hero_image', 'default') }}">
@endif
<h1>{{ $item->title }}</h1>
<p>{{ $item->description }}</p>
</div>
<div class='content'>
{!! $item->renderBlocks(false) !!}
</div>
And will let Twill know which view we'll use for the preview
class ArticleController extends ModuleController
{
protected $moduleName = 'articles';
protected $previewView = 'site.articles.show';
}

Next STEPS

  • FE with CSS
  • Repeater inside Blocks and inside Main Structure
  • Browsers inside Blocks
  • Generic Pages
Show previous messages

April 8, 2019 at 1:39am
Hey , you are right that field is needed in the migration if you want to re-order the Invitees. If you take a look at the source code, it is there. I've updated the tutorial to include that in the migration.
Edited
  • reply
  • like

April 8, 2019 at 9:57pm
I understand. And Twill seems great but I'm now looking for other solutions since chatting and researching cost me to much time. I think a straight forward doc would be nice. For everyone to get started.
  • Create CRUD Module
  • Add Tags
  • BelongsTo x
  • HasMany x
  • Remove Published
  • Change default title field to e.g name
Edited
  • reply
  • like

April 11, 2019 at 3:54pm
Ok thanks for confirming. Just wanted to point it out for other people following this tutorial :). Looking forward for the front end stuff next time. When do you think the next tutorial will be released? I get that there are other priorities so don't feel rushed I'm just curious
  • reply
  • like

April 13, 2019 at 3:17pm
I'm polishing a bit the article and will come out hopefuly on monday.
  • reply
  • like

April 15, 2019 at 1:17pm
This is exactly what I needed. Thanks for this work . Looking forward to the next one.
  • reply
  • like

April 30, 2019 at 3:50pm
looking forward for the next part! Thanks!
  • reply
  • like

May 2, 2019 at 2:39pm
When will the new part be released?
  • reply
  • like

May 3, 2019 at 8:27pm
I want to say thankyou for taking your time to create these very useful guides, hoping to see more of them.
Edited
  • reply
  • like

May 14, 2019 at 12:56pm
Can't wait for the next tutorial, it's been very helpfull.
  • reply
  • like

May 23, 2019 at 12:03pm
Any plan / Schedule on tutorial for the font end
Edited
like-fill
2
  • reply
  • like

July 16, 2019 at 8:51pm
next tutorial for front end and user registration using twill auth
  • reply
  • like

July 22, 2019 at 1:53am
Thank you please keep these coming,.... info on fully customizing the index view too please!
  • reply
  • like

August 23, 2019 at 7:49pm
I get a fatal error having tried to copy this project and run it locally. A few tweaks to the config to get the database and migrations and an admin user setup worked fine but when trying to run the login page I get...
ErrorException (E_ERROR) The Mix manifest does not exist. (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php) (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php) (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php)
Edited
  • reply
  • like

August 28, 2019 at 7:54pm
I get a fatal error having tried to copy this project and run it locally. A few tweaks to the config to get the database and migrations and an admin user setup worked fine but when trying to run the login page I get...
ErrorException (E_ERROR) The Mix manifest does not exist. (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php) (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php) (View: twilldemo\vendor\area17\twill\views\partials\head.blade.php)
  • reply
  • like

September 3, 2019 at 2:47pm
Implementing the middle part of code to create a browser field outside of a block structure to link two data models together isn't working for me. I've tried following the code in two separate projects now and in both cases the interface appears, updating the record appears to work with the yellow success msg, yet no record is ever written to the association table that was created so when you reload the edit page in twill the linked record is not there.
I've created a post on this here, if anyone can suggest ways to troubleshoot ?
  • reply
  • like

September 19, 2019 at 6:36am
About title . As we can see on this example a title can be some person's name. And I want to have a table with title that is not translateable and description is. And I can't achieve that because I must have the title translateable - this title is the field when you add a record. I don't want to repeat the name of a person in other languages because I suppose it doesn't change and I don't use Slugs in this example.
  • reply
  • like

September 19, 2019 at 1:12pm
Hi , there are multiple ways to go about disabling the title field translation on a translated model but I think the simplest approach is to create a create.blade.php file in the views folder for that Twill module, next to the form file, with the following code: ('twill::partials.create', ['translateTitle' => false]).
  • reply
  • like

September 21, 2019 at 2:23pm
Hey , thank you!
  • reply
  • like

September 25, 2019 at 12:22pm
Hey, It works fine. But there is the Permalink I see I can switch it off and I added it manually with 'translated' => false but permalink gets value [object Object], when I fill this field manually the value gets saved in slug in table (not with translations of course). But this permalink doesn't appear under title (because not HasSlugs?) and when I edit gain title it get value [object Object]. Permalink Prefix is working well. I just wanted to have a translatable module but with Title/Slug that is not translatable. Is that possible?
  • reply
  • like

April 27, 2020 at 5:50am
Where can I find next step? this year is 2020.
like-fill
1
  • reply
  • like

April 28, 2020 at 9:30pm
Please visit our YouTube Channel for more examples and tutorials https://www.youtube.com/channel/UC5rhdsVLs4vPVuy_dAaRzhA
like-fill
1
  • reply
  • like

April 29, 2020 at 5:26am
Please visit our YouTube Channel for more examples and tutorials https://www.youtube.com/channel/UC5rhdsVLs4vPVuy_dAaRzhA
Yes I am doing with Youtube now. But there's no code samples or repository. When I encountered problems, It's hard to find where and what I missed or mistaken :(
  • reply
  • like

May 19, 2020 at 9:45pm
This is very helpful, thank you --- I'm noticing that if I use the browser to attach some belongsToMany relations though, it always seems to order them by ID and not the position field on the pivot table. Any thought on a way around this? I've tried just adding ->withPivot('position')->orderBy('pivot_table.position', 'ASC') on the relation in my model but it does nothing.
Edited
  • reply
  • like

July 5, 2020 at 11:42pm
Hey , In an application I have a model Person and another Group and a pivot table group_person. Person has a position attribute also the group_person table. What I'm using for retrieving the people ordered using the pivot table is: In the Group model
public function people()
{
return $this->belongsToMany(Person::class, 'group_person')
->orderBy('group_person.position');
}
  • reply
  • like

July 10, 2020 at 10:13am
How do I include a 'browser' formField which is placed in a different admin route prefix (group?) For 'browser' to work, I need to have all the modules in root or same level, else i get error - Route [admin.pages.featuredLinks.browser] not defined. Inside the _browser.blade.php file
In the required repository added this line: protected $browsers = [ 'featuredLinks' => [ 'routePrefix' => 'shared' ] ];
Edited
  • reply
  • like
Show more messages