menu

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 1 year ago)
The first part of this turorial is available here

Step by step - Creating a Twill app.

thumbsup
26
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

April 1, 2019 at 5:43pm
thanks for this!
like-fill
1
  • reply
  • like
frontend, repeaters and generic pages wooohooo :D excited for that
  • reply
  • like
Thank you ! This is really awesome!
  • reply
  • like

April 2, 2019 at 1:59pm
Thanks for this tutorial. Is this the way how to handle all types of relationships within Twill?
  • reply
  • like

April 3, 2019 at 7:39pm
I hit this error every time I reach this part
public function afterSave($object, $fields)
{
$this->updateBrowser($object, $fields, 'invitees');
parent::afterSave($object, $fields);
}
public function getFormFields($object)
{
$fields = parent::getFormFields($object);
$fields['browsers']['invitees'] = $this->getFormFieldsForBrowser($object, 'invitees');
return $fields;
}
ERROR Call to a member function map() on null
  • reply
  • like
like-fill
1
  • reply
  • like
I'm working on a big round of updates in the form fields section of our documentation to provide you all with the default way of using them to save and retrieve content, there's too much left for interpretation at the moment.
Edited
  • reply
  • like
that's not the only way, browser is mostly suited for belongsToMany relationship but you can also make it work for belongsTo. Select and radios for belongsTo as well. Repeaters for hasMany or even polymorphic relationships. Even submodules with dot notation are supported with hasMany for use cases where repeaters in the same form are just not scalable.
  • reply
  • like
But again, at the moment the code does much much more than what the documentation expose and I'm really hopeful that we can fix it by getting this feedback here. I'm glad we opened our Spectrum and that it is generating all this content, thank you all!
  • reply
  • like
Thank you and I've added that missing step to the tutorial.
  • reply
  • like
also something that should be noted. Nothing will work without translations enabled. Possibly a bug?
  • reply
  • like
what does without translations enabled entails to you? If your translatable configuration only defines a single locale code the UI doesn't show anything relating to translations. If you're absolutely sure you will never want to translate your content, you would not use the translations traits and table. But if you have even a slight doubt you might need to in the future, I would recommend to use the feature with a single language to avoid having to migrate columns in the future.
Edited
like-fill
1
  • reply
  • like

April 4, 2019 at 5:15pm
Thats a perfectly reasonable argument . I just had to change more of the default behavior to remove the 'en' from the slug. Unless I setup something wrong. I made sure I only had one language set in preferences. My use case is medium to large size local businesses and churches with no need to have more than one language presented.
  • reply
  • like

April 6, 2019 at 8:34pm
Hey guys, while following this tutorial I received the error (Your submission could not be validated, please fix and retry) in the log file it said (Column not found: 1054 Unknown column 'position' in 'field list' (SQL: insert into article_invitee). I added the column and then it worked. I was curious why in my case a position column was needed in the pivot table article_invitee and not for other people following this tutorial. I think it's because I rearranged the order of the invitees once in the user interface. Is this reasoning correct or is it something else?
Edited
  • reply
  • like

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
Show more messages