ganyicz / bond
Modern component authoring for Laravel Blade and Alpine.js
Installs: 36
Dependents: 0
Suggesters: 0
Security: 0
Stars: 111
Watchers: 2
Forks: 4
Open Issues: 1
Language:JavaScript
Requires
- laravel/framework: ^11.0 | ^12.0
README
⚠️ Alpha release:
This package is currently under active development and not yet intended for production use; It is best to try on a fresh Laravel project. Feedback and contributions are welcome! Learn more about the goals and the current development here
Bond brings modern component authoring to Laravel Blade and Alpine.js. It introduces a few features inspired by React and Vue, making it easier to write structured, maintainable components. Bond also ships with a VS Code extension that adds syntax highlighting, autocomplete, and error checking.
Installation
Install Bond into your project using Composer:
composer require ganyicz/bond
Next, add the following lines to your resources/js/app.js
file. Bond will compile all scripts extracted from your Blade files here.
import './bootstrap'; + import '../../vendor/ganyicz/bond/js/alpine'; + import 'virtual:bond';
Finally, update your vite.config.js
to register Bond:
import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import tailwindcss from '@tailwindcss/vite'; + import bond from './vendor/ganyicz/bond/dist/vite-plugin'; export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, }), tailwindcss(), + bond(), ], ... });
If you're using Alpine.js CDN, make sure it's registered after your app.js
file.
<head> <!-- This needs to be first --> @vite(['resources/css/app.css', 'resources/js/app.js']) <!-- Alpine CDN goes here --> <script src="//unpkg.com/alpinejs" defer></script> </head>
If you're using Livewire, you don't need to worry about this, as Livewire automatically loads Alpine.js for you. Just make sure your app.js
file is registered somwhere in the <head>
tag.
VS Code Extension
For the best development experience, install the Bond VS Code extension. It provides syntax highlighting, autocomplete, and error checking for both Bond components and Alpine.js attributes. The extension will be open-sourced in a future release.
Make sure to also install the official Laravel extension.
Bond will only load in files with language mode set to Blade
.
Quick guide
Make sure you are familiar with both Alpine.js and Laravel Blade, as Bond builds on top of these technologies and supports all their features.
When ready, start your Vite development server:
npm run dev
Creating a new component
Bond is intended to be used within Blade components. Create one in resources/views/components
. You can use the following Artisan command:
php artisan make:view components.alert
Then add a <script setup>
tag and {{ $attributes }}
as described below.
<script setup>
This is where you'll define props, state, and functions for this component. Bond’s Vite plugin will scan all Blade files within your resources/views
directory, extract code from <script setup>
tags and bundle it into a single JavaScript file. The script tags will never actually be rendered on the page.
<script setup> mount((props: { // }) => ({ // })) </script>
Important
Since the code will get extracted into a JavaScript file, you cannot use Blade within the script tag.
The component gets automatically mounted on the element where you place your {{ $attributes }}
. In the background, Bond just adds directives like x-data
and x-component
to your attributes to identify and initialize the component.
<div {{ $attributes }}> <!-- This will be the root --> ... </div>
Important
Components using <script setup>
are isolated from the outer scope by design. To pass data in, use props or slots.
Defining props
Props let you pass reactive data from outside into your component. Define them in the callback parameter of the mount function with a type annotation:
<script setup> mount((props: { message: string, }) => ({ init() { console.log(props.message) } })) </script>
Props can be accessed inside the mount
function and in the template using the props.
prefix.
<span x-text="props.message"></span>
Passing props
Once defined, any Alpine or Livewire variable can be passed in as a prop using the x-
prefix:
<x-alert x-message="errors[0]" x-open="$wire.open" />
All props are two-way bound by default. The message
inside the component will reactively update when the outer variable changes and vice versa. (This behavior might change in future releases.)
You can also pass static values like numbers, strings, or functions.
<x-number-input x-step="0.1" x-format="'9.99'" x-onincrement="() => console.log('incremented')" />
Caution
Prop names cannot conflict with Alpine.js directives. For example, you cannot use on
, model
, or text
. For the full list of reserved names, see the list of directives in Alpine.js docs.
Defining data
To define data and functions on your component, use the object returned from the mount
function callback. When referencing data within that object, you must use the this
keyword. In the template, you can access data directly without any prefix.
<script setup> mount((props: { message: string, }) => ({ uppercase: false, toggle() { this.uppercase = !this.uppercase }, get formattedMessage() { return this.uppercase ? props.message.toUpperCase() : props.message }, })) </script> <div {{ $attributes }}> <button x-on:click="toggle">Toggle</button> <span x-text="formattedMessage"></span> </div>
Using components
After you've defined your component, you can use it in your Blade templates like this:
<div x-data="{ errors: ['You have exceeded your quota'] }"> <x-alert x-message="errors[0]" /> </div>
While this is a simple example, you can use these patterns to build complex components with multiple props, two-way data binding, integrate with Livewire and more.
Slots
Bond components are isolated, which also applies to slots. Any content you pass into a slot will NOT have access to the parent scope by default.
Let's use the example from before, but instead of passing the message as a prop, we will use a slot.
<!-- This will throw an error --> <div x-data="{ errors: ['You have exceeded your quota'] }"> <x-alert> <span x-text="errors[0]"></span> <!-- errors is undefined --> </x-alert> </div>
The errors
variable will not be accessible.
To make the slot behave as expected, wrap it in an element with an x-slot
directive inside your component.
<div {{ $attributes }}> <div x-slot>{{ $slot }}</div> </div>
Important
Directives used on an element with x-slot
will also use the outer scope, not just its children.
Imports
Since Bond compiles <script setup>
tags with Vite, you can use any import supported in a JavaScript/TypeScript file:
<script setup> // NPM module import { twMerge } from 'tailwind-merge' // Local file import { createTodo } from '@/todo' // Raw file content import check from '@resources/img/icons/check.svg?raw' // Types import type { TodoItem } from '@/types' mount(...) </script>
The @
alias points to resources/js
by default. You can customize the aliases in your vite.config.js
file, however this will not reflect in the VS Code extension at the moment and only the default alias will work in your IDE.
Make sure to always import types using the import type
syntax to avoid issues.
Else statement
Alpine does not support else statements out of the box. Bond adds partial support for it. The limitation is that the template with the x-else
directive must be inside the parent template.
<template x-if="active"> Your subscription is active <template x-else> Your subscription has expired </template> </template>
A simpler custom syntax for control statements is planned:
<!-- This is NOT yet supported --> <if {active}> Your subscription is active <else> Your subscription has expired </if>
Icons
Dynamic icons in Alpine usually require rendering all icons and toggling with x-show
.
With Bond, you can import SVGs and render them dynamically with x-html
:
<script setup> import check from '@resources/img/icons/check.svg?raw' import circle from '@resources/img/icons/circle.svg?raw' mount(() => ({ icons: {check, circle} })) </script> <div {{ $attributes }}> <span x-html="todo.done ? icons.check : icons.circle"></span> </div>
This will likely be revisited in the next release with a more structured approach to icons.
TypeScript
Bond uses TypeScript to provide a terse syntax for props and also to power the IDE features. However, it disables the strict
mode by default. This means you are not forced to use types. You can write regular JavaScript without getting type errors, but still get autocomplete and type hints in your IDE.
Options for both enabling strict
mode and fully opting out of TypeScript will be available in the future.
Adding types to properties
If a property is not initialized immediately, use the as
keyword to define its type:
<script setup> mount(() => ({ value: null as number | null, })) </script>
Important
TypeScript syntax is only supported inside <script setup>
. Alpine expressions are not bundled, and using types in them will cause runtime errors.
Roadmap
Warning
The following features are planned but not yet implemented. Feedback and contributions are encouraged.
Attribute syntax
Bond will support a JSX-like syntax for attributes. This makes it easier to visually distinguish between HTML/Blade attributes and reactive bindings. This syntax will be optional.
<!-- This is NOT yet supported --> <input model={value} onchange={() => console.log($el.value)} disabled={value < 0} class=({ 'bg-gray-200': value < 0, 'bg-blue-500': value >= 0 }) >
The example above would be compiled to:
<input x-model="value" x-on:change="() => console.log($el.value)" x-bind:disabled="value < 0" x-bind:class="{ 'bg-gray-200': value < 0, 'bg-blue-500': value >= 0 }" >
Control statement tags
Alpine requires wrapping conditional or loop logic in <template>
tags, which can be verbose. Bond will introduce a cleaner syntax that will also enable real else
statements.
The syntax below was designed to be visually distinct from Blade directives and its HTML-like structure will be easy to compile into Alpine.js code.
<!-- This is NOT yet supported --> <if {active}> Your subscription is active <else> Your subscription has expired </if> <for {account in accounts}> ... </for>
Compiled output:
<template x-if="active"> Your subscription is active <template x-else> Your subscription has expired </template> </template> <template x-for="account in accounts"> </template>
Interpolation
Bond will add support for inline template interpolation. This lets you write expressions directly in HTML with curly braces, similar to Vue or React:
<!-- This is NOT yet supported --> <div x-template>Hello, {name}</div>
{name}
will be replaced with the actual value at runtime.
Cross-file IntelliSense (VS Code)
The Bond VS Code extension will provide autocomplete and type checking for props on the outside of the component, ensuring type safety across files.
Common error diagnostics (VS Code)
The Bond VS Code extension will include diagnostics for common errors in Alpine.js attributes, such as a missing key in a x-for
loop, one root element per <template>
tag, and more.
Blade enhancements
While Bond primarily augments Alpine.js, several Blade-specific enhancements would be beneficial to improve modularity and organization.
Multiple components per file
<!-- This is NOT yet supported --> @export <div>This is (x-summary)</div> @export('title') <h3>This is (x-summary.title)</h3> @export('header') <div>This is (x-summary.header)</div> @endexport
Imports and aliases
<!-- This is NOT yet supported --> @import('app::checkout.summary', 'summary') <x-summary> <x-summary.header> <x-summary.title>Summary</x-summary.title> </x-summary.header> </x-summary>
<!-- This is NOT yet supported --> @import('app::checkout.summary') <x-header> <x-title>Summary</x-title> </x-header>