Visão geral do MadWire
state, action, model, render parcial.
MadWire é o sistema de reatividade server-first do MAD
framework — inspirado no Livewire (Laravel), construído sobre
Mad\Component\MadComponent + Blade puro. Ele permite construir
interfaces ricas sem escrever JavaScript: propriedades
públicas da classe viram state, métodos públicos viram
actions, e a UI se atualiza automaticamente a cada interação.
Como funciona
Todo MadComponent renderiza um wrapper com o estado atual
serializado e criptografado. Cada interação (mad:click,
mad:model, mad:change, mad:submit)
dispara um POST AJAX para um endpoint dedicado — /app/_mad-wire
no admin, /public/_mad-wire em páginas públicas reativas
(MadSitePage) — processado por
Mad\Component\MadComponentHandler.
1. Browser carrega HTML com <div mad-component="X" mad-state="..." mad-id="...">
2. Usuario interage (mad:click, mad:model, mad:change, mad:submit)
3. mad-livewire.js monta um POST com:
mad_state = state criptografado atual (AES-256-GCM)
mad_id = identificador do wrapper no DOM
mad_action = nome do metodo PHP a chamar
mad_model = valores coletados de todos os [mad:model] do wrapper
4. MadComponentHandler decripta o state, hidrata a instancia, aplica os
mad:model recebidos, chama o metodo (resolvendo argumentos por nome/tipo)
5. O diff de estado (snapshot ANTES vs DEPOIS da action) gera ops parciais
automaticas; a action pode tambem retornar um MadResponse com ops explicitas
6. Resposta JSON { id, html?, ops[], partial? } - JS aplica no DOM sem reload
Hello, MadWire
Um contador clássico já mostra os dois lados do ciclo: estado público e ações.
Controller
use Mad\Component\MadComponent;
class Contador extends MadComponent
{
protected static string $wrapper = self::INTERNAL;
public int $count = 0;
public function increment(): void
{
$this->count++;
}
public function decrement(): void
{
$this->count = max(0, $this->count - 1);
}
public function reset(): void
{
$this->count = 0;
}
protected function view(): string|array
{
return 'admin.contador';
}
}
View — admin/contador.blade.php
<mad-page-container>
<mad-page-header title="Contador" icon="hash" />
<mad-page-content>
<div style="text-align:center;padding:2rem;">
<h1 style="font-size:5rem;">{{ $that->count }}</h1>
<div style="display:flex;gap:.5rem;justify-content:center;">
<mad-btn mad:click="decrement" variant="outline">-</mad-btn>
<mad-btn mad:click="reset" variant="ghost">Reset</mad-btn>
<mad-btn mad:click="increment" variant="primary">+</mad-btn>
</div>
</div>
</mad-page-content>
</mad-page-container>
Pronto. Cada clique em + ou - chama o método PHP
correspondente, atualiza $count, e o DOM reflete o novo valor
— sem JS manual, sem AJAX explícito, sem state global no cliente.
Conceitos centrais
| Conceito | Mecanismo | Descrição |
|---|---|---|
state |
props públicas | Toda propriedade public do MadComponent vira state. Serializada e criptografada (AES-256-GCM) a cada response. |
action |
método público | Métodos públicos podem ser chamados do client via mad:click, mad:submit ou mad:change. |
model |
mad:model |
Bind bidirecional input ↔ prop pública. Sincroniza no próximo POST (ou imediatamente com .live). |
auto-bind |
retorno void |
Action que retorna void tem o state comparado antes/depois — o framework infere e emite as ops parciais sozinho. |
partial render |
MadResponse |
->html(), ->val(), ->manageRow(), ->bind() atualizam só o seletor alvo — sem re-render total do componente. |
Lifecycle
Inspirado no Livewire 3. Na primeira carga (GET):
boot() → mount() → rendering() →
render() → rendered() → dehydrate().
Em requisições AJAX (mad:click/mad:model):
boot() → hydrate() →
updating()/updated() → a action → de novo
rendering() → render() → rendered() →
dehydrate().
class MeuForm extends MadComponent
{
public function boot(): void
{
// Roda em TODA requisicao (primeira carga + cada action AJAX)
// Bom para setup compartilhado: ler config, resolver tenant, etc.
}
public function mount(): void
{
// Roda so na primeira carga (GET) - NAO roda de novo em actions
$this->produtos = Produto::where('ativo', '1')->get();
}
// Action - chamada via mad:click="onSalvar"
public function onSalvar(): MadResponse
{
// ...
return MadToast::success('Salvo!');
}
// Hook auto-bind - roda apos cada prop publica mudar via mad:model
public function updated(string $name, mixed $value): void
{
// ex: recalcular total quando "quantidade" muda
}
// Hook auto-bind - roda ANTES da prop mudar
public function updating(string $name, mixed $old, mixed $new): void
{
// ex: validar o novo valor antes de aceitar
}
protected function view(): string|array
{
return 'admin.meu-form';
}
}
Tudo que é prop public entra no state — e o state vai
para o client (mesmo cifrado). Senhas, tokens e dados sensíveis
devem ficar em props private/protected
(resolvidas de novo em boot()/hydrate()
a cada request), nunca em props públicas.
Estado serializado e criptografado
O state vai no atributo mad-state do wrapper, serializado em
JSON e cifrado com Mad\Http\MadStateCrypt — AES-256-GCM
(criptografia autenticada): o conteúdo não é apenas assinado, é
ilegível para o client, e qualquer bit-flip é detectado
pela tag de autenticação GCM (o decrypt simplesmente falha e o componente
é re-hidratado do zero). Por isso o endpoint /app/_mad-wire /
/public/_mad-wire não depende de CSRF para validar o
state em si — ele já é autenticado criptograficamente — mas o token CSRF
(header X-CSRF-TOKEN) ainda é exigido pela rota.
A API JavaScript do MadWire
A engine client vive em packages/mad-framework/assets/mad-livewire.js
(publicada em public/lib/mad/mad-livewire.js) e expõe cinco métodos
para os casos em que uma diretiva não dá conta — normalmente dentro de um
mad-on:*, de um callback de biblioteca externa ou de um
MadResponse->script():
| Método | Faz |
|---|---|
MadWire.call(idOuEl, action = '', params = []) | Dispara uma action como se fosse um mad:click |
MadWire.set(idOuEl, prop, valor) / MadWire.set(idOuEl, objeto) | Seta prop(s) e re-renderiza — o mesmo que o atalho $set |
MadWire.refresh(idOuEl) | Reenvia os mad:model atuais e re-renderiza, sem chamar action — o mesmo que $refresh |
MadWire.configure(opcoes) | Ajusta a config global; hoje só debounceDelay (default 300 ms, usado pelo mad:model.live) |
MadWire.initLoadingElements(root) | Re-esconde os [data-mad-loading] (e mostra os [data-mad-loading-remove]) dentro de root — necessário só depois de injetar HTML por fora do morph da engine |
O primeiro argumento aceita tanto o mad-id do wrapper (string)
quanto um elemento qualquer dentro dele — nesse caso a engine sobe
a árvore até achar o [mad-component] mais próximo.
MadWire não é window.MadWire
MadWire é declarado como const no escopo do
módulo mad-livewire.js — não é publicado
em window. Ele funciona em qualquer código carregado
depois no mesmo escopo global de script clássico, mas
window.MadWire é undefined, e um
if (window.MadWire) ... vai silenciosamente não fazer
nada. Escreva MadWire.set(...) direto, sem o prefixo, e
sem guard em window. (O mesmo não vale
para Mad e MadErrorModal, esses sim
publicados em window pelos mesmos bundles.)
Quando usar MadWire
Route::get em routes/web.php, ou uma MadSitePage) — sem o overhead de um componente reativo.Route::apiResource → ApiResourceController), que devolve JSON puro.