Админка
please wait

O Alpine.js traz reatividade ao HTML sem a complexidade de frameworks completos. Com apenas 15 KB, é perfeito para adicionar interatividade a páginas renderizadas no servidor, aplicações multipágina ou em qualquer contexto em que precise apenas do JavaScript estritamente necessário. Este guia aborda a criação de UIs interativas com Alpine.js na perspetiva de um developer sénior.

Porquê Alpine.js

O Alpine oferece vantagens convincentes:

  1. Pegada mínima: ~15 KB gzipped, carrega instantaneamente
  2. Sem etapa de build: Funciona diretamente em HTML
  3. Declarativo: O comportamento vive em atributos HTML
  4. Sintaxe ao estilo do Vue: Padrões familiares, menor complexidade
  5. Perfeito para melhoria incremental: Adicione a páginas existentes renderizadas no servidor

Primeiros Passos

Instalação

CDN (mais simples):

<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

NPM:

npm install alpinejs
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

Diretivas Principais

x-data: Estado do Componente

<!--Definir componente com estado-->
<div x-data="{ open: false, count: 0, name: '' }">
<p>Count: <span x-text="count"></span></p>
<button @click="count++">Increment</button>
</div>
<!--Componentes aninhados têm âmbito isolado-->
<div x-data="{ outer: 'parent' }">
<div x-data="{ inner: 'child' }">
<span x-text="outer"></span> <!--Funciona-->
<span x-text="inner"></span> <!--Funciona-->
</div>
</div>

x-on: Tratamento de Eventos

<!--Eventos de clique-->
<button x-on:click="open = !open">Toggle</button>
<button @click="open = !open">Toggle (shorthand)</button>
<!--Eventos de teclado-->
<input @keydown.enter="submitForm()" />
<input @keydown.escape="cancel()" />
<!--Modificadores-->
<button @click.prevent="handleClick()">Prevent Default</button>
<button @click.stop="handleClick()">Stop Propagation</button>
<button @click.once="runOnce()">Run Once</button>
<button @click.debounce.500ms="search()">Debounced</button>
<!--Vários eventos-->
<input @focus="focused = true" @blur="focused = false" />

x-bind: Atributos Dinâmicos

<!--Associar atributos-->
<button x-bind:disabled="isLoading">Submit</button>
<button :disabled="isLoading">Submit (shorthand)</button>
<!--Classes dinâmicas-->
<div :class="{ 'active': isActive, 'hidden': !isVisible }"></div>
<div :class="isActive ? 'bg-blue-500' : 'bg-gray-500'"></div>
<!--Estilos dinâmicos-->
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>

x-model: Ligação Bidirecional

<div x-data="{ message: '', selected: 'a', checked: false, items: [] }">
<!--Entrada de texto-->
<input type="text" x-model="message" />
<p x-text="message"></p>
<!--Modificadores-->
<input x-model.lazy="message" /> <!--Sincronizar ao perder o foco-->
<input x-model.number="count" /> <!--Converter para número-->
<input x-model.debounce.500ms="search" /> <!--Debounce-->
<!--Seleção-->
<select x-model="selected">
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>
<!--Checkbox-->
<input type="checkbox" x-model="checked" />
<!--Várias checkboxes-->
<input type="checkbox" value="item1" x-model="items" />
<input type="checkbox" value="item2" x-model="items" />
<!--Radio-->
<input type="radio" value="a" x-model="selected" />
<input type="radio" value="b" x-model="selected" />
</div>

x-show e x-if: Renderização Condicional

<!--x-show: Alterna o CSS de display (mantém-se no DOM)-->
<div x-show="isVisible">I'm visible</div>
<!--Com transições-->
<div x-show="open" x-transition>Fade in/out</div>
<div x-show="open"
x-transition.duration.500ms
x-transition:enter.opacity
x-transition:leave.opacity>
Custom transition
</div>
<!--x-if: Remove efetivamente do DOM (tem de usar template)-->
<template x-if="showForm">
<form>...</form>
</template>

x-for: Ciclos

<div x-data="{ items: ['Apple', 'Banana', 'Orange'] }">
<template x-for="(item, index) in items" :key="index">
<div>
<span x-text="index + 1"></span>.
<span x-text="item"></span>
</div>
</template>
</div>
<!--Com objetos-->
<template x-for="user in users" :key="user.id">
<div>
<span x-text="user.name"></span>
<span x-text="user.email"></span>
</div>
</template>

Propriedades Mágicas

$el: Elemento Atual

<button @click="$el.innerText = 'Clicked!'">Click me</button>

$refs: Referências a Elementos

<div x-data="{ }">
<input type="text" x-ref="input" />
<button @click="$refs.input.focus()">Focus Input</button>
<button @click="$refs.input.value = ''">Clear</button>
</div>

$watch: Observador Reativo

<div x-data="{ count: 0 }" x-init="$watch('count', value => console.log('Count is:', value))">
<button @click="count++">Increment</button>
</div>

$dispatch: Eventos Personalizados

<!--O filho dispara o evento-->
<div x-data @notify="alert($event.detail.message)">
<button @click="$dispatch('notify', { message: 'Hello!' })">
Notify Parent
</button>
</div>
<!--Comunicação entre componentes-->
<div x-data @item-selected.window="selectedItem = $event.detail">
Selected: <span x-text="selectedItem"></span>
</div>
<div x-data>
<button @click="$dispatch('item-selected', 'Item 1')">Select Item 1</button>
</div>

$nextTick: Após Atualização do DOM

<div x-data="{ items: [] }">
<button @click="
items.push('New Item');
$nextTick(() => {
$refs.list.scrollTop = $refs.list.scrollHeight;
});
">Add Item</button>
<ul x-ref="list" style="max-height: 100px; overflow-y: auto;">
<template x-for="item in items">
<li x-text="item"></li>
</template>
</ul>
</div>

Estado Global com Stores

<script>
document.addEventListener('alpine:init', () => {
Alpine.store('cart', {
items: [],
add(item) {
this.items.push(item);
},
remove(index) {
this.items.splice(index, 1);
},
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
});
Alpine.store('user', {
name: '',
isLoggedIn: false,
login(name) {
this.name = name;
this.isLoggedIn = true;
},
logout() {
this.name = '';
this.isLoggedIn = false;
}
});
});
</script>
<!--Usar store em qualquer lugar-->
<div x-data>
<template x-if="$store.user.isLoggedIn">
<p>Welcome, <span x-text="$store.user.name"></span>!</p>
</template>
<p>Cart items: <span x-text="$store.cart.items.length"></span></p>
<p>Total: $<span x-text="$store.cart.total"></span></p>
<button @click="$store.cart.add({ name: 'Product', price: 9.99 })">
Add to Cart
</button>
</div>

Componentes Reutilizáveis

Extrair para Funções

<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
toggle() {
this.open = !this.open;
},
close() {
this.open = false;
}
}));
Alpine.data('counter', (initialCount = 0) => ({
count: initialCount,
increment() {
this.count++;
},
decrement() {
if (this.count > 0) this.count--;
}
}));
});
</script>
<!--Usar componentes reutilizáveis-->
<div x-data="dropdown">
<button @click="toggle()">Menu</button>
<ul x-show="open" @click.outside="close()">
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
<div x-data="counter(10)">
<button @click="decrement()">-</button>
<span x-text="count"></span>
<button @click="increment()">+</button>
</div>

Exemplos Práticos

Janela Modal

<div x-data="{ showModal: false }">
<button @click="showModal = true">Open Modal</button>
<!--Fundo do modal-->
<div x-show="showModal"
x-transition.opacity
@click="showModal = false"
class="fixed inset-0 bg-black bg-opacity-50 z-40">
</div>
<!--Conteúdo do modal-->
<div x-show="showModal"
x-transition
@click.stop
@keydown.escape.window="showModal = false"
class="fixed inset-0 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded-lg max-w-md w-full mx-4">
<h2 class="text-xl font-bold mb-4">Modal Title</h2>
<p>Modal content goes here.</p>
<button @click="showModal = false" class="mt-4 btn">Close</button>
</div>
</div>
</div>

Componente de Separadores

<div x-data="{ activeTab: 'tab1' }">
<div class="tabs">
<button @click="activeTab = 'tab1'"
:class="{ 'active': activeTab === 'tab1' }">
Tab 1
</button>
<button @click="activeTab = 'tab2'"
:class="{ 'active': activeTab === 'tab2' }">
Tab 2
</button>
<button @click="activeTab = 'tab3'"
:class="{ 'active': activeTab === 'tab3' }">
Tab 3
</button>
</div>
<div class="tab-content">
<div x-show="activeTab === 'tab1'">Content for Tab 1</div>
<div x-show="activeTab === 'tab2'">Content for Tab 2</div>
<div x-show="activeTab === 'tab3'">Content for Tab 3</div>
</div>
</div>

Obter Dados (Fetch)

<div x-data="{
users: [],
loading: false,
error: null,
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (e) {
this.error = 'Failed to load users';
} finally {
this.loading = false;
}
}
}" x-init="fetchUsers()">
<div x-show="loading">Loading...</div>
<div x-show="error" x-text="error" class="text-red-500"></div>
<ul x-show="!loading && !error">
<template x-for="user in users" :key="user.id">
<li x-text="user.name"></li>
</template>
</ul>
<button @click="fetchUsers()" :disabled="loading">Refresh</button>
</div>

Validação de Formulários

<form x-data="{
form: { email: '', password: '' },
errors: {},
submitted: false,
validate() {
this.errors = {};
if (!this.form.email) {
this.errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(this.form.email)) {
this.errors.email = 'Invalid email format';
}
if (!this.form.password) {
this.errors.password = 'Password is required';
} else if (this.form.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
}
return Object.keys(this.errors).length === 0;
},
submit() {
if (this.validate()) {
this.submitted = true;
// Submit form...
}
}
}" @submit.prevent="submit()">
<div>
<input type="email" x-model="form.email" placeholder="Email"
:class="{ 'border-red-500': errors.email }">
<p x-show="errors.email" x-text="errors.email" class="text-red-500 text-sm"></p>
</div>
<div>
<input type="password" x-model="form.password" placeholder="Password"
:class="{ 'border-red-500': errors.password }">
<p x-show="errors.password" x-text="errors.password" class="text-red-500 text-sm"></p>
</div>
<button type="submit">Sign In</button>
<p x-show="submitted" class="text-green-500">Form submitted!</p>
</form>

Integração com Backend

Laravel Blade

<div x-data="{
items: @js($items),
deleteItem(id) {
if (confirm('Delete this item?')) {
fetch(`/items/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
}).then(() => {
this.items = this.items.filter(i => i.id !== id);
});
}
}
}">
<template x-for="item in items" :key="item.id">
<div>
<span x-text="item.name"></span>
<button @click="deleteItem(item.id)">Delete</button>
</div>
</template>
</div>

Principais Conclusões

  1. Comece de forma simples: x-data, x-on, x-show cobrem a maioria dos casos
  2. Use stores para estado global: Evita «prop drilling»
  3. Extraia componentes reutilizáveis: Alpine.data() para padrões
  4. Tire partido de modificadores: .prevent, .stop, .debounce poupam código
  5. Combine com renderização no servidor: O Alpine melhora, não substitui
  6. Mantenha a lógica em HTML: Essa é a força do Alpine

O Alpine.js é perfeito quando precisa de interatividade sem o overhead de um framework SPA completo — especialmente para melhorar progressivamente aplicações renderizadas no servidor.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive