Alpine.js is a relatively new front-end framework that promises the reactive and declarative nature of big frameworks like Vue.js or React.js without having to run npm, compile scripts, configure webpack all in a nice 7.37kb cdn hosted file. You get to keep your DOM, and sprinkle in behavior as you see fit. Think of it like Tailwind for JavaScript. After using Alpine on several enterprise projects, I can testify to its ease of use and scalability.
According to the Alpine.js docs, its syntax is almost entirely borrowed from Vue.js (and by extension Angular.js), so if you already know either of these frameworks there is a very low learning curve to getting started.
Before you begin this tutorial, make sure you have the following set up/installed:
A Laravel 7 application already installed
A Twilio account
To demonstrate some of the basic functionality of Alpine.js, I thought it appropriate to create a simple Twilio Verify form that allows users to enter their phone number, receive a code and verify their phone number, all while showing some nice transitions and conditionals with Alpine.js in a Livewire component.
First, you will need to have the Twilio PHP SDK installed along with Laravel Livewire. Run the following commands in your terminal to install these dependencies inside of your Laravel application folder:
$ composer require twilio/sdk
$ composer require livewire/livewire
As with any Laravel project handling secure credentials, you will need to store them safely in a dotenv file. Grab your Twilio Account SID and Auth Token from the console and add them to your .env
:
TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXX
Next, create a Livewire component. This command will generate a Livewire class and Blade file for our markup.
$ php artisan make:livewire verifyApi
Add a Livewire route to routes/web.php
:
Route::livewire('verify', 'verify-api');
If it’s not present, create a layout file resources/views/layouts/app.blade.php
and add the following code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Twilio Livewire</title>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
@livewireStyles
</head>
<body class="h-screen w-screen flex items-center justify-center bg-gray-900">
@yield('content')
@livewireScripts
</body>
</html>
In your VerifyApi
class, located in the app/Http/Livewire
folder, add the following methods and properties:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class VerifyApi extends Component
{
public $phone_number;
public $code;
public $status = '';
public $error = '';
public function sendCode()
{
$twilio = resolve('TwilioClient');
$verification = $twilio
->verify
->v2
->services(getenv('TWILIO_VERIFICATION_SID'))
->verifications
->create('+1' . str_replace('-', '', $this->phone_number), "sms");
$this->status = $verification->status;
}
public function verifyCode()
{
$twilio = resolve('TwilioClient');
try {
$verification_check = $twilio
->verify
->v2
->services(getenv('TWILIO_VERIFICATION_SID'))
->verificationChecks
->create($this->code, // code
["to" => '+1' . str_replace('-', '', $this->phone_number)]
);
} catch (\Exception $e) {
$this->error = $e->getMessage();
}
if ($verification_check->valid == false) {
$this->error = 'That code is invalid, please try again.';
}else{
$this->error = '';
$this->status = $verification_check->status;
}
}
public function render()
{
return view('livewire.verify-api');
}
}
app/AppServiceProvider.php
in the register
method you can add the following:$this->app->singleton('TwilioClient', function(){
return new Client(
getenv('TWILIO_ACCOUNT_SID'),
getenv('TWILIO_AUTH_TOKEN')
);
});
Now, whenever we need to get the Twilio Client, we can resolve
it out of the container, and will already have our credentials attached.
$twilio = resolve('TwilioClient');
Before we can begin using this code to make verification requests, we need to head over to the Twilio console and create our Verify Service. Click create and make a friendly name for your app (this will display in the SMS messages sent to your users). Once that is done, Twilio will display an SID
. Copy this and add it to your .env
.
TWILIO_VERIFICATION_SID=VA6c1fb42e927c73664eaff944caef1df4
Besides the render
method in our VerifyApi class (which simply tells Laravel which blade file to render), we have added a sendCode
and a verifyCode
method. These methods will be called from our blade file by submitting a form. Add the following markup to the verify-api.blade.php
file in the views/livewire
folder:
<div class="bg-gray-200 p-5 rounded shadow-lg w-1/3">
<div >
<form wire:submit.prevent="sendCode">
<x-inputs.group label="Verify Your Phone Number" for="phone_number">
<x-inputs.text for="phone_number" wire:model="phone_number"/>
</x-inputs.group>
<button type="submit" class="w-full bg-indigo-500 text-white rounded-lg my-4 py-2 uppercase font-bold">Verify</button>
</form>
</div>
<div>
<form wire:submit.prevent="verifyCode" class="w-full">
<x-inputs.group label="Enter code" for="code">
<x-inputs.text for="code" wire:model="code"/>
</x-inputs.group>
<button type="submit" class="w-full bg-indigo-500 text-white rounded-lg my-4 py-2 uppercase font-bold">Verify Code</button>
</form>
</div>
</div>
In order for these x-input
components to work, create a directory inside ‘resources/viewscalled
componentsand another directory there called
inputs. Create two new files,
group.blade.phpand
text.blade.php`.
Inside of group.blade.php
add the following code:
@props(['label', 'for'])
<div {{ $attributes->merge(['class' => 'sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-gray-200 sm:pt-5']) }}>
<label for="{{ $for }}"
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
{{ $label }}
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
{{ $slot }}
</div>
</div>
Andi inside text.blade.php
add this code:
@props([
'for',
'type' => 'text',
'value' => ''
])
<div class="rounded-md shadow-sm">
<input
type="{{ $type }}"
value="{{ $value }}"
{{ $attributes->merge(['class' => 'bg-white border border-gray-200 py-2 px-3 rounded-lg w-full block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5']) }}
id="{{ $for }}"
/>
</div>
NOTE: We are using anonymous blade components that are explained in detail here, as well as Tailwind CSS for styling.
Take a moment and start your Laravel application by running php artisan serve
. Navigate to http://127.0.0.1:8000/verify and you should see the following:
If you enter your phone number in the first input and click “Verify” you should receive an SMS message with a code, and by entering that code into the second field and clicking “Verify Code” we will have executed the functionality of this nifty tool!
Now, I don’t know about you, but this is nowhere near prime-time ready for me. What if there is an error? How do we tell the user the code is sent and verified accordingly? This is what Alpine was created for. We can achieve this flexibility (and much more) without having to manage any dependencies in webpack/mix
and without running npm run …
.
Adding Alpine.js is as easy as adding a single line of code into the head
section of your layout app.blade.php
file:
<script src="https://cdn.jsdelivr.net/gh/alpinejs/[email protected]/dist/alpine.min.js" defer></script>
That’s it!
Much like Vue.js, we can add properties to our DOM and initialize them with some default values and conditionally change our site when these properties are changed.
Back in the blade file, change the outer-most div to the following:
<div x-data="{status: '{{$status}}', error: '{{$error}}'}" class="bg-gray-200 p-5 rounded shadow-lg w-1/3">
This x-data
attribute tells Alpine what we are watching and sets the initial values. In this case status
and error
are now properties we can use anywhere inside of this div. To demonstrate, let’s hide the second form, until a phone number is submitted and the code is sent. Replace the inner code in resources/views/livewire/verify-api.blade.php
with the following:
<div x-show="status == ''">
<form wire:submit.prevent="sendCode">
<x-inputs.group label="Verify Your Phone Number" for="phone_number">
<x-inputs.text for="phone_number" wire:model="phone_number"/>
</x-inputs.group>
<button type="submit" class="w-full bg-indigo-500 text-white rounded-lg my-4 py-2 uppercase font-bold">Verify</button>
</form>
</div>
<div x-show="status == 'pending'">
<form wire:submit.prevent="verifyCode" class="w-full">
<x-inputs.group label="Enter code" for="code">
<x-inputs.text for="code" wire:model="code"/>
</x-inputs.group>
<button type="submit" class="w-full bg-indigo-500 text-white rounded-lg my-4 py-2 uppercase font-bold">Verify Code</button>
</form>
</div>
x-show
will display that portion of your code, based on a condition. In this case, if the status
is an empty string (which is set in our VerifyApi class on the public property), it will display the first form. When that status
changes to pending
, we hide the first form and show the second form. Refreshing your page should show the following:
Now, when we enter our phone number and submit the form, the first form hides and the second form is shown conditionally! Perfect!
Change your whole blade file to the following:
<div x-data="{status: '{{$status}}', error: '{{$error}}'}" class="bg-gray-200 p-5 rounded shadow-lg w-1/3">
<h3 class="uppercase text-sm text-center text-indigo-500 font-bold"
:class="{ 'text-indigo-500' : status === 'pending', 'text-green-500' : status === 'approved'}"
>{{$status}}
</h3>
<h3 x-show="error != ''" class="uppercase text-sm text-center text-red-500 font-bold">{{$status}}</h3>
<div x-show="status == ''">
<form wire:submit.prevent="sendCode">
<x-inputs.group label="Verify Your Phone Number" for="phone_number">
<x-inputs.text for="phone_number" wire:model="phone_number"/>
</x-inputs.group>
<button type="submit" class="w-full bg-indigo-500 text-white rounded-lg my-4 py-2 uppercase font-bold">Verify</button>
</form>
</div>
<div x-show="status == 'pending'"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-90"
>
<form wire:submit.prevent="verifyCode" class="w-full">
<x-inputs.group label="Enter code" for="code">
<x-inputs.text for="code" wire:model="code"/>
</x-inputs.group>
<button type="submit" class="w-full bg-indigo-500 text-white rounded-lg my-4 py-2 uppercase font-bold">Verify Code</button>
</form>
</div>
</div>
You can now start to see the incredible power and flexibility we can obtain with Alpine. Transitions and conditionally binding classes to elements is only scraping the surface of what Alpine can do!
If you want to extend this tutorial, you can check out Alpine’s docs on Github, Awesome Alpine (a curated list of awesome resources related to Alpine.js) ,iterate through an array with x-for
, add two-way binding with x-model
, and even add @click
handlers to your buttons, all without creating a separate component or running npm!
What will you build with Alpine? Are you already using it in the wild? Leave some comments below and keep the conversation going!