Objectives:
If you have been working with Twill for some time, you may already be familiar with the php artisan twill:update
command. This command takes Twill's precompiled assets (JS, CSS, etc.) and copies them to your
project's public/assets/admin
folder.
To develop custom Vue.js components for Twill, we can forget about twill:update
and take control of the build step
with the following commands:
twill:build
-> Build Twill assets with custom Vue components/blockstwill:dev
-> Hot reload Twill assets with custom Vue components/blocksWe'll start by creating a simple HelloWorld
component and building it with twill:build
.
You will need a module in your project to follow along. We'll use pages
as an example in this text.
Create the following file in your project:
File:
resources/assets/js/components/HelloWorld.vue
1 2<template> 3 <!-- eslint-disable --> 4 <div> 5 Hello {{ name }} 6 </div> 7</template> 8 9<script>10 /* eslint-disable */11 12 export default {13 props: ['name']14 }15</script>
Why eslint-disable
? Twill's own build tools come with a pretty strict ESLint configuration out of the box. This
ensures a good internal consistency in the code of Twill's own components, but will give you an error if your components
don't follow Twill's formatting rules.
The eslint-disable
comments allow you to bypass the formatting rules for your own components. Try it for yourself,
turn it on/off and see what you like!
Run:
1php artisan twill:build
This will start by installing Twill's NPM dependencies in vendor/area17/twill/node_modules
. Twill will check if the
node_modules folder exist and if needed will run npm install. If you want to force npm install you can use:
1php artisan twill:build --install
This is only for Twill's own dependencies, you don't need to modify what is being installed in there. For your own
dependencies in your custom components, you can run npm install
directly into your project.
When the build is done, you can use the new component in your forms:
File:
resources/views/admin/pages/form.blade.php
1@extends('twill::layouts.form')2 3@section('contentFields')4<a17-hello-world name="World"></a17-hello-world>5@stop
In your markup, all custom components must have the a17-
prefix. In my personal opinion, it's also best to use an
additional prefix for your components, such as a17-custom-
. This will avoid all possible conflicts with Twill's
built-in components, which also share the a17-
prefix.
The component in action:
twill:dev
When developing components, you can take advantage of twill:dev
for fast rebuilds,
hot reloading and auto refresh in the browser:
1php artisan twill:dev
This will watch for changes in your resources/assets/js
directory and rebuild accordingly.
Note for Sail/Docker users you may get the following error when running twill:dev
:
1Error: $SHELL environment variable is not set.
In this case, you can prefix the twill:dev
command like this:
1export SHELL=/usr/bin/bash && php artisan twill:dev
After running twill:dev
, you need to inform Twill that you are in development
mode by setting the following variables in your .env
file:
1TWILL_DEV_MODE=true2TWILL_DEV_MODE_URL="http://localhost:8080"
TWILL_DEV_MODE_URL
is used by Twill to access the development assets through the hot reloading server. You don't need
to access this address in your browser, simply access your development site as before (e.g. http://my-app.test)
For Valet users the above configuration should work out of the box.
For Homestead/Vagrant users if you are running twill:dev
inside of the VM, make sure to set TWILL_DEV_MODE_URL
to your actual development URL (e.g. TWILL_DEV_MODE_URL="http://my-app.test:8080"
)
For Sail/Docker users if you are running twill:dev
inside of the container, make sure to expose the 8080
port in
your docker-compose.yml
or Dockerfile
configuration.
The HelloWorld
component is obviously not very useful. Custom components can be used to add interactivity to your
Twill admin forms. Here's a more complete example of a CustomTabs
component to organize your form fields in tabs:
This reuses a bit of CSS from other parts of Twill (ie. .box__filter
).
File:
resources/assets/js/components/CustomTabs.vue
1 2<template> 3 <!-- eslint-disable --> 4 <div class="custom-tabs"> 5 <ul class="box__filter"> 6 <li v-for="tab of tabs"> 7 <a 8 href="#" 9 :class="{ 's--on': activeTab === tab.name }"10 @click.prevent="activateTab(tab.name)"11 >12 {{ tab.label }}13 </a>14 </li>15 </ul>16 <slot></slot>17 </div>18</template>19 20<script>21 /* eslint-disable */22 23 export default {24 props: {25 tabs: { type: Array },26 },27 28 data () {29 return {30 activeTab: null31 }32 },33 34 mounted () {35 if (this.tabs.length > 0) {36 this.activateTab(this.tabs[0].name)37 }38 },39 40 methods: {41 activateTab (name) {42 this.$el.querySelectorAll('.custom-tab').forEach(tab => {43 tab.classList.remove('is-active')44 });45 46 const newActiveTab = this.$el.querySelector(`.custom-tab--${name}`)47 48 if (newActiveTab) {49 newActiveTab.classList.add('is-active')50 this.activeTab = name51 }52 }53 },54 }55</script>56 57<style scoped>58 .custom-tabs {59 margin-top: 7px;60 }61 62 .custom-tab {63 display: none;64 }65 66 .custom-tab.is-active {67 display: initial;68 }69</style>
File:
resources/views/admin/pages/form.blade.php
1@extends('twill::layouts.form') 2 3@section('contentFields') 4 <a17-custom-tabs :tabs="[ 5 { name: 'header', label: 'Header' }, 6 { name: 'body', label: 'Body' }, 7 { name: 'footer', label: 'Footer' }, 8 ]"> 9 <div class="custom-tab custom-tab--header">10 <x-twill::input11 name="header_title"12 label="Header title"13 :translated="true"14 />15 16 <x-twill::input17 name="header_subtitle"18 label="Header subtitle"19 :translated="true"20 />21 </div>22 23 <div class="custom-tab custom-tab--body">24 <p>** Add body form fields here **</p>25 </div>26 27 <div class="custom-tab custom-tab--footer">28 <p>** Add footer form fields here **</p>29 </div>30 </a17-custom-tabs>31@stop
Form fields are at the core of the content management experience in any CMS. In Twill, form fields are usually made of 2 parts: a Vue.js component that hooks into Twill's Vuex store, and a Blade view that acts as an interface to this component.
Here's a complete example of a custom_number
form field, based on
the vue-numeric package:
1npm install vue-numeric --save
File:
resources/assets/js/components/CustomNumber.vue
1 2<template> 3 <!-- eslint-disable --> 4 <a17-inputframe 5 :error="error" 6 :note="note" 7 :label="label" 8 :name="name" 9 :required="required" 10 > 11 <div class="form__field"> 12 <vue-numeric 13 :precision="precision" 14 :currency="currency" 15 :decimal-separator="decimalSeparator" 16 :thousand-separator="thousandSeparator" 17 :name="name" 18 v-model="value" 19 @blur="onBlur" 20 ></vue-numeric> 21 </div> 22 </a17-inputframe> 23</template> 24 25<script> 26 /* eslint-disable */ 27 28 import InputMixin from '@/mixins/input' 29 import FormStoreMixin from '@/mixins/formStore' 30 import InputframeMixin from '@/mixins/inputFrame' 31 32 import VueNumeric from 'vue-numeric' 33 34 export default { 35 mixins: [InputMixin, InputframeMixin, FormStoreMixin], 36 37 components: { VueNumeric }, 38 39 props: { 40 name: { 41 type: String, 42 required: true, 43 }, 44 initialValue: { 45 type: Number, 46 default: 0, 47 }, 48 precision: { 49 type: Number, 50 default: 2, 51 }, 52 currency: { 53 type: String, 54 default: '$', 55 }, 56 decimalSeparator: { 57 type: String, 58 default: '.', 59 }, 60 thousandSeparator: { 61 type: String, 62 default: '', 63 }, 64 }, 65 66 data () { 67 return { 68 value: this.initialValue, 69 } 70 }, 71 72 methods: { 73 updateFromStore (newValue) { 74 if (typeof newValue === 'undefined') newValue = '' 75 76 if (this.value !== newValue) { 77 this.value = newValue 78 } 79 }, 80 updateValue (newValue) { 81 if (this.value !== newValue) { 82 this.value = newValue 83 84 this.saveIntoStore() 85 } 86 }, 87 onBlur (event) { 88 const newValue = event.target.value 89 this.updateValue(newValue) 90 }, 91 }, 92 } 93</script> 94 95<style lang="scss" scoped> 96 .form__field { 97 display: flex; 98 align-items: center; 99 padding: 0 15px;100 overflow: visible;101 102 input {103 @include resetfield;104 width: 100%;105 height: 43px;106 line-height: 43px;107 color: inherit;108 padding: 0;109 }110 }111</style>
File:
resources/views/admin/partials/form/_custom_number.blade.php
1@php 2 $precision = $precision ?? '2'; 3 $currency = $currency ?? '$'; 4 $decimalSeparator = $decimalSeparator ?? '.'; 5 $thousandSeparator = $thousandSeparator ?? ''; 6@endphp 7 8<a17-custom-number 9 label="{{ $label }}"10 :precision="{{ $precision }}"11 currency="{{ $currency }}"12 decimal-separator="{{ $decimalSeparator }}"13 thousand-separator="{{ $thousandSeparator }}"14 @include('twill::partials.form.utils._field_name')15 in-store="value"16></a17-custom-number>17 18@unless($renderForBlocks || $renderForModal || (!isset($item->$name) && is_null($formFieldsValue = getFormFieldsValue($form_fields, $name))))19@push('vuexStore')20 window['{{ config('twill.js_namespace') }}'].STORE.form.fields.push({21 name: '{{ $name }}',22 value: {!! json_encode($item->$name ?? $formFieldsValue) !!}23 })24@endpush25@endunless
File:
resources/views/admin/pages/form.blade.php
1@extends('twill::layouts.form')2 3@section('contentFields')4 @formField('custom_number', [5 'name' => 'magic_number',6 'label' => 'Magic Number',7 'thousandSeparator' => ',',8 ])9@stop
This form field is functional but very limited, on purpose. For example, it is not translatable. At this time, the best way to explore what's possible with form fields is to dig into Twill's built-in form components.
Here are a few good places to start exploring:
The first thing to say about custom Vue.js blocks is that you probably don't need them! Since Twill 2.0, custom blocks
can be created entirely with Blade views. The twill:make:block
command can scaffold a Blade block for you:
1php artisan twill:make:block banner
This will create the following file in your project:
File:
resources/views/admin/blocks/banner.blade.php
1@twillBlockTitle('Banner') 2@twillBlockIcon('text') 3@twillBlockGroup('app') 4 5<x-twill::input 6 name="title" 7 label="Title" 8/> 9 10// ...
You can add all of Twill's built-in form fields in your blocks, as well as any custom components and form fields that you create. This is the reason why creating custom Vue.js blocks may not be necessary.
Here's an example of our custom_number
field inside of a custom Blade block:
File:
resources/views/admin/blocks/price.blade.php
1@twillBlockTitle('Price')2@twillBlockIcon('text')3@twillBlockGroup('app')4 5@formField('custom_number', [6 'name' => 'magic_number',7 'label' => 'Magic Number',8 'thousandSeparator' => ',',9])
Nevertheless, if you wish to explore custom Vue.js blocks, you can use the twill:blocks
command to get going.
Add the @twillBlockCompiled
directive to your Blade view:
File:
resources/views/admin/blocks/banner.blade.php
1@twillBlockTitle('Banner')2@twillBlockIcon('text')3@twillBlockGroup('app')4@twillBlockCompiled('true')5 6// ...
Then run :
1php artisan twill:blocks
This will create the following file in your project:
File:
resources/assets/js/blocks/BlockBanner.vue
1 2<template> 3 <!-- eslint-disable --> 4 <div class="block__body"> 5 <a17-locale type="a17-textfield" 6 :attributes="{ label: 'Title', name: fieldName('title'), type: 'text', inStore: 'value' }"></a17-locale> 7 </div> 8</template> 9 10<script>11 import BlockMixin from '@/mixins/block'12 13 export default {14 mixins: [BlockMixin]15 }16</script>
This file is effectively your Blade template, rendered into a Vue.js component shell. How cool is that?
From this point, you can customize the new Vue.js component in any way you want. Keep in mind that your custom block is limited to the following area in the Block Editor (shown in blue):
When you are done, run:
1php artisan twill:build
Then the custom block is usable in the block editor:
File:
resources/views/admin/pages/form.blade.php
1@extends('twill::layouts.form')2 3@section('contentFields')4 // ...5 <x-twill::block-editor :blocks="['banner']"/>6@stop