Tailwire
Build web apps without having to write HTML, CSS, or JavaScript! Powered by Laravel Livewire and Tailwind.
Imagine a world where you no longer have to constantly context switch between HTML, CSS, and Javascript. Where all of your code can reside in self-contained PHP component classes. Even your database migration logic can stay inside of your models. It's kind of like SwiftUI for PHP!
Requirements:
- Laravel 8
- PHP 8
- NPM
Features:
- Expressive HTML builder written using PHP
- Tailwind styling & configuration built in
- Laravel Livewire component property & action wiring
- Handy directives for if, each, include, etc. statements
- Commands for install, auth, components, CRUD, models, & automatic migrations
- Automatic component routing
- Automatic model migrations
- Automatic user timezones
- Easy app versioning
- PWA capabilities (standalone launcher, icons, etc.)
- Pull down refresh (iOS PWA)
- Infinite scrolling
- Google Recaptcha integration
- Heroicon integration
- & more!
Docs:
Links:
- Support: GitHub Issues
- Contribute: GitHub Pulls
- Donate: PayPal
Installation
Create a new Laravel 8 project:
laravel new my-app
Configure your .env
app, database, and mail values:
APP_*
DB_*
MAIL_*
Require Tailwire via composer:
composer require redbastie/tailwire
Install Tailwire:
php artisan tailwire:install
Commands
Install Tailwire
php artisan tailwire:install
Installs the base Tailwire components, User model & factory, config files, icons, PWA manifest, Tailwind CSS & config, JavaScript assets, webpack, and runs the necessary NPM commands.
Generate auth scaffolding
php artisan tailwire:auth
Generates auth scaffolding components including login, logout, register, password reset, home, and basic User CRUD.
Generate a component
php artisan tailwire:component {class} {--full}
Generates a Tailwire component class. You can use a subdirectory when specifying the component class, e.g. Admin/Page
. Use the --full
option to generate a full page component, or omit it to generate a basic component.
Examples:
php artisan tailwire:component VehicleListItem
php artisan tailwire:component Vehicle --full
php artisan tailwire:component Admin/Insurance --full
Generate simple CRUD
php artisan tailwire:crud {class}
Generates CRUD components, a model, and factory for the specified model class. You can also use a subdirectory when specifying the class, which will place the model and components in the corresponding directory/namespace.
Examples:
php artisan tailwire:crud Vehicle
php artisan tailwire:crud Admin/Insurance
Generate a Tailwire model
php artisan tailwire:model {class}
Generates a Tailwire model and factory which comes with the automatic migration method and factory definition included. As with other commands, you can specify a subdirectory for the class.
Examples:
php artisan tailwire:model Vehicle
php artisan tailwire:model Admin/Insurance
Run automatic migrations
php artisan tailwire:migrate {--fresh} {--seed}
Runs automatic migrations for all of your Tailwire models which have a migration
method. This uses Doctrine to automatically diff your database and apply the necessary changes. Optionally use --fresh
to wipe the database, and --seed
to run your seeders afterwards. Note that if you still want to use traditional Laravel migration files, they will run before Tailwire automatic migrations when using the tailwire:migrate
command.
Usage
Routing full page components
class Login extends Component
{
public $routeUri = '/login';
public $routeName = 'login';
public $routeMiddleware = 'guest';
Specify public $route*
properties in your Tailwire component in order to enable automatic routing. A minimum of $routeUri
is required in order to enable automatic routing for the component. Available properties include $routeUri
, $routeName
, $routeMiddleware
(string or array), $routeDomain
, $routeWhere
(array).
If using route parameters, be sure to include them in a mount
method:
class Vehicle extends Component
{
public $routeUri = '/vehicle/{vehicle}';
public $routeName = 'vehicle';
public $routeMiddleware = ['auth'];
public $vehicle;
public function mount(Vehicle $vehicle)
{
$this->vehicle = $vehicle;
}
Extending layout components
class Login extends Component
{
public $viewTitle = 'Login';
public $viewExtends = 'layouts.app';
Specify a $viewExtends
property which uses dot notation to point to the component that this component will extend. In this example, the $viewExtends
property is pointing to the Layouts/App
component.
In the Layouts/App
component, the $v->yield()
method is used in order to render the child component inside:
class App extends Component
{
public function view(View $v)
{
return $v->body(
$v->header('Header content')->class('text-xl'),
$v->yield(),
$v->footer('Footer content')->class('text-sm'),
)->class('bg-gray-100');
Building HTML elements
class Home extends Component
{
public function view(View $v)
{
return $v->section(
$v->h1('Home')->class('text-xl px-6 py-4'),
$v->p('You are logged in!')->class('p-6')
)->class('bg-white rounded-lg shadow divide-y');
}
Tailwire uses an expressive syntax in order to build HTML elements. As you can see, the $v
variable is used to construct each element in the view. The first method of a $v
chain consists of the HTML element name. A list of available HTML element names can be found here: HTML Element Reference
After the first method, each additional chained method represents an attribute of the element. For example, creating an image:
$v->img()->src(asset('images/fav-icon.png'))->class('w-5 h-5')
This would translate to an image with an src
of the asset URL, using the Tailwind classes w-5 h-5
for styling. A list of available HTML element attributes can be found here: HTML Attribute Reference
In this example, you can see that the img
method does not accept any parameters, because it does not use a closure tag. Elements that use a closure tag, such as div
, do accept ...$content
parameters, which you can use in order to build content inside:
$v->div(
$v->p('Hello')->class('text-red-600'),
$v->p('World')->class('text-green-600'),
)->class('bg-blue-100')
Styling elements via Tailwind
$v->icon('refresh')->class('animate-spin text-gray-400 w-5 h-5 mx-auto')
Specify the Tailwind classes for an element within the chained class()
method.
Now you might be thinking, "but there isn't autocomplete!" Fortunately, VSCode and PHPStorm both offer plugins which will add Tailwind autocomplete to your Tailwire components. I personally use TailwindCSS Autocomplete for PHPStorm, which works great.
Wiring elements via Livewire
Along with HTML attributes, Tailwire also allows you to wire up your elements to make them reactive via Livewire. The method names are specified according to Livewire conventions, which can be found here: Laravel Livewire docs
Modelling data
$v->input()->type('email')->id('email')->wireModelDefer('email')
Tailwire components contain a public $model
array which will contain the data that is modelled through elements like inputs, selects, etc.
You can grab the data using the $this->model()
helper method:
$email = $this->model('email');
If your modelled data is an array, you can use the dot notation to grab values from the array:
$userName = $this->model('user.name');
You validate the $model
data using $this->validate()
:
$validated = $this->validate([
'email' => ['required', 'email'],
]);
When checking for validation errors, you can use the $this->error()
method to check if a validation error exists for the modelled data:
$v->if($this->error('email'),
fn() => $v->p($this->error('email'))->class('text-xs text-red-600')
)
Performing actions
class Counter extends Component
{
public $count = 0;
public function view(View $v)
{
return $v->div(
$v->button('Increment Count')->wireClick('incrementCount'),
$v->p($this->count)->class('text-blue-600'),
);
}
function incrementCount()
{
$this->count++;
}
As you can see, you can wire up particular actions via the wire*
methods as well. This includes things like clicking, polling, submitting, etc. If your action uses parameters, simply specify them in the wire*
method:
public function view(View $v)
{
return $v->div(
$v->button('Increment Count')->wireClick('incrementCount', 2),
$v->p($this->count)->class('text-blue-600'),
);
}
function incrementCount($amount)
{
$this->count += $amount;
}
The beauty of this is that it allows you to keep all of your logic inside the Tailwire component classes themselves! No more switching between tons of files and languages and wondering how things happen.
Using directives & other methods
The View $v
variable also allows you to use some handy directives inside your view
method, such as if statements, each loops, includes, and more.
If statements:
$v->if(Auth::guest(),
fn() => $v->p('You are signed out.')
)->else(
fn() => $v->p('You are signed in!)
)
Each loops:
$v->each(Vehicle::all(),
fn(Vehicle $vehicle) => $v->div(
$v->p($vehicle->name),
$v->p($vehicle->created_at)->class('text-xs text-gray-600')
)
)->empty(
fn() => $v->p('No vehicles found.')
),
Including partial components:
$v->include('user-list-item', $user),
Heroicons (list of available icons here):
$v->icon('cog')->class('text-blue-600 w-5 h-5'),
Pull down to refresh indicator (for iOS PWA's):
$v->pullDownRefresh(
$v->icon('refresh')->class('animate-spin text-gray-400 w-5 h-5 mx-auto')
)->class('mb-4'),
Infinite scrolling indicator:
$v->infiniteScroll(
$v->icon('refresh')->class('animate-spin text-gray-400 w-5 h-5 mx-auto')
)->class('mt-4'),
Google Recaptcha:
$v->div(
$v->recaptcha(),
$v->if($this->error('recaptcha'),
fn() => $v->p($this->error('recaptcha'))->class('text-xs text-red-600')
)
)->class('space-y-1 overflow-hidden'),
Guides
Pull down refresh
$v->pullDownRefresh(
$v->icon('refresh')->class('animate-spin text-gray-400 w-5 h-5 mx-auto')
)->class('mb-4'),
If a user has scrolled -100px
from the top of the page, the pullDownRefresh
element will display briefly before the entire page is reloaded. This is useful for PWA's, when the user adds your web app to their home screen and needs a way to refresh the page.
Infinite scrolling
public function view(View $v)
{
return $v->section(
$v->h1('Vehicles')->class('text-2xl mb-2'),
$v->each($this->query()->paginate($this->perPage),
fn(Vehicle $vehicle) => $v->div(
$v->p($vehicle->name),
$v->p(timezone($vehicle->created_at))->class('text-xs text-gray-600')
)->class('px-6 py-4')
),
$v->if($this->query()->count() > $this->perPage,
fn() => $v->infiniteScroll(
$v->icon('refresh')->class('animate-spin text-gray-400 w-5 h-5 mx-auto')
)->class('mt-4')
)
);
}
public function query()
{
return Vehicle::query()->orderBy('name');
}
Each Tailwire component contains a $perPage
public property which is incremented if the hidden infiniteScroll
element is present on the page and the user scrolls 100px
from the bottom. After it is incremented, the component should load more items and hide the infiniteScroll
element again. See how query()
, paginate()
, count()
, and $perPage
are used in the example above.
Google Recaptcha
public function view(View $v)
{
return $v->section(
$v->h1('Create Vehicle')->class('text-xl px-6 py-4'),
$v->form(
$v->div(
$v->label('Name')->for('name'),
$v->input()->id('name')->wireModelDefer('name')
->class($this->error('name') ? 'border-red-500' : 'border-gray-300'),
$v->if($this->error('name'),
fn() => $v->p($this->error('name'))->class('text-xs text-red-600')
)
)->class('space-y-1'),
$v->div(
$v->recaptcha(),
$v->if($this->error('recaptcha'),
fn() => $v->p($this->error('recaptcha'))->class('text-xs text-red-600')
)
)->class('space-y-1 overflow-hidden'),
$v->button('Create Vehicle')->type('submit')->class('text-white bg-blue-600 px-4 py-2')
)->wireSubmitPrevent('create')->class('space-y-4')
)->class('bg-white rounded-lg shadow divide-y');
}
public function create()
{
$validated = $this->validate([
'name' => ['required'],
'recaptcha' => ['required', new RecaptchaRule],
]);
Vehicle::query()->create($validated);
}
The recaptcha
element will automatically model a recaptcha
response value to your component once it is completed. Note the use of the RecaptchaRule
, which checks to see if the response was correct via the Recaptcha SDK.