Two-way binding
mad:model bidirecional input ↔ prop pública.
mad:model é o mecanismo de two-way binding do
MadWire: sincroniza o valor de um input no DOM com uma prop pública do
MadComponent atual. A direção DOM → PHP é
esta página; a direção oposta — PHP → DOM via auto-bind e
MadResponse — já é coberta em
Auto-bind
e MadResponse — todas as ops.
mad-model (hífen)
mad:model (dois pontos) sai do navegador e sincroniza
com o PHP. mad-model (hífen) é alias de
x-model do Alpine — 100% client-side, nunca chega no
servidor. Detalhes da família mad-* (Alpine) em
Diretivas mad-*.
Sintaxe básica
Funciona em qualquer elemento com value — input nativo ou
qualquer componente mad-*-field, que já injeta
mad:model="{name}" sozinho (a partir do atributo
name) quando você não declara um mad:model
explícito.
MAD__BLADE_COMMENT__1__
<input type="text" mad:model="busca" value="{{ $that->busca }}">
MAD__BLADE_COMMENT__2__
<mad-input-field name="busca" label="Busca" />
MAD__BLADE_COMMENT__3__
<mad-input-field name="busca" mad:model="busca" />
class ProdutoListagem extends MadComponent
{
public string $busca = ''; // prop publica = ponto de bind
public function onFiltrar(): void
{
$this->itens = Produto::where('nome', 'like', "%{$this->busca}%")->get();
}
}
O ciclo completo — DOM → PHP → DOM
1. Usuario digita "tenis" no input
mad:model="busca" guarda o valor em data-mad-model — NADA eh enviado ainda
2. Usuario clica um botao com mad:click (ou submete um <form mad:submit>,
ou muda um <select mad:change>) no MESMO wrapper
mad-livewire.js coleta TODOS os [data-mad-model]/[data-mad-model-live]
do wrapper e monta o payload:
mad_state = state criptografado atual (AES-256-GCM)
mad_id = id do wrapper no DOM
mad_action = "onFiltrar"
mad_model = { busca: "tenis" }
POST /app/_mad-wire (ou /public/_mad-wire em pagina publica reativa)
3. MadComponentHandler::handle()
a. decripta mad_state, instancia a classe, chama boot() + hydrate()
b. _applyModelValues(mad_model):
- filtra mass-assignment (_isModelAssignable)
- dispara updating('busca', $old, 'tenis') + updatingBusca('tenis', $old)
- fill(): settype('tenis', gettype($old)) e atribui $this->busca
- dispara updated('busca', 'tenis') + updatedBusca('tenis')
c. chama onFiltrar() (so roda se mad_action nao for vazio)
d. diff do snapshot ANTES vs DEPOIS gera ops de auto-bind automaticamente
4. Resposta JSON { id, mad_state (novo), ops[] } — JS aplica no DOM,
sem reload de pagina
mad:model sozinho não dispara nada
Sem .live, mad:model só acumula
o valor no client — o POST só acontece quando outro
gatilho do mesmo wrapper dispara (mad:click,
mad:submit, mad:change ou
$refresh). É por isso que mad:model é o
padrão "seguro": não gera tráfego a cada tecla.
mad:model vs mad:model.live
| Diretiva | Quando sincroniza | Chama alguma action? |
|---|---|---|
mad:model="prop" |
No próximo mad:click/mad:submit/mad:change/$refresh do wrapper |
Só a que disparou o POST |
mad:model.live="prop" |
Sozinho, a cada evento input, com debounce fixo de 300ms |
Nenhuma — POST com mad_action vazio: só sincroniza a prop e deixa o auto-bind gerar as ops do diff |
O caso clássico de .live é busca/preview reativo — o hook
updatedBusca() (ver próxima seção) faz o trabalho, sem
precisar de nenhum método chamado por mad:click:
MAD__BLADE_COMMENT__4__
<mad-input-field name="busca" mad:model.live placeholder="Buscar produto...">
<span class="muted">Buscando: @madBind('busca')</span>
class ProdutoListagem extends MadComponent
{
public string $busca = '';
public array $itens = [];
public function mount(): void
{
$this->itens = Produto::where('ativo', '1')->limit(50)->get()->all();
}
// Hook por-prop — roda DEPOIS que $busca já foi atualizada pelo mad:model.live,
// mesmo sem nenhuma action ter sido chamada explicitamente.
public function updatedBusca(string $value): void
{
$this->itens = $value === ''
? Produto::where('ativo', '1')->limit(50)->get()->all()
: Produto::where('nome', 'like', "%{$value}%")->get()->all();
}
}
.live
Cada elemento com .live gera um POST a cada pausa de
digitação (mesmo que pequena, 300ms) — use com moderação. Para um
formulário inteiro, prefira mad:model simples + um
mad:submit/mad:click único; reserve
.live para o(s) campo(s) que realmente precisam de
feedback imediato.
Hooks updating / updated
Toda vez que mad:model traz um valor novo, o framework chama,
nesta ordem, antes de aplicar — updating()
(genérico) e updating{Prop}() (específico, se existir) — e
depois de aplicar, o par equivalente
updated()/updated{Prop}().
updating* não transformam o valor
O retorno de updating()/updating{Prop}() é
ignorado — o valor que chega do cliente é aplicado
do jeito que chegou (com settype() coercionando para o
tipo original da prop). Use updating* para validar,
logar ou disparar efeitos colaterais; use updated*
(ou normalize o valor dentro da própria action) se precisa mudar o
que fica salvo na prop.
class ProdutoForm extends MadComponent
{
public string $nome = '';
public float $preco = 0;
public int $quantidade = 1;
public float $total = 0;
// Generico — roda para QUALQUER prop alterada via mad:model neste request
public function updating(string $prop, mixed $old, mixed $new): void
{
logger()->debug("mad:model {$prop}: " . json_encode($old) . ' -> ' . json_encode($new));
}
// Especifico — so para "preco". Assinatura: (newValue, oldValue)
public function updatedPreco(float $newValue): void
{
$this->total = $newValue * $this->quantidade;
}
public function updatedQuantidade(int $newValue): void
{
$this->total = $this->preco * $newValue;
}
}
Bind direto em campos de um MadForm
Quando o componente expõe public MadForm $form;, notação com
ponto liga o input diretamente a um campo do form (sem precisar declarar
uma prop pública dedicada para cada campo) — fill() reconhece
o prefixo e grava em $this->form->fields[...]:
<input type="text" mad:model="form.cidade" value="{{ $that->form->get('cidade') }}">
<input type="text" mad:model="form.cep" value="{{ $that->form->get('cep') }}">
class EnderecoForm extends MadComponent
{
public MadForm $form;
public function mount(): void
{
$this->form = new MadForm();
}
public function onSalvar(): MadResponse
{
// form.cidade / form.cep já estão em $this->form->fields neste ponto
Endereco::create($this->form->fields);
return (new MadResponse())->toast('Endereço salvo!', 'success');
}
}
mad:model em cada tipo de input
mad-livewire.js serializa o valor de forma diferente conforme
o tipo de elemento — tudo isso é transparente quando você usa os
componentes mad-*-field; só importa se está montando o input
na mão:
| Tipo de elemento | Valor enviado em mad_model[prop] |
|---|---|
| texto, número, data, textarea, select simples | el.value direto |
<input type="checkbox"> | "1" marcado / "0" desmarcado |
<select multiple> | JSON array dos valores selecionados |
mad-checkbox-group-field | JSON array dos valores marcados |
mad-radio-field (grupo) | valor do <input type="radio"> marcado |
mad-checklist-field | JSON array dos IDs marcados |
Um mad:model dentro de uma linha de
<mad-field-list> ou <mad-detail-form>
é serializado à parte, como linhas (mad_field_lists /
mad_detail_forms) — não como valor solto em
mad_model. Evita que o campo (frequentemente vazio) do
editor inline sobrescreva um campo de mesmo nome no master.
Segurança — o que mad:model nunca consegue sobrescrever
Todo valor de mad_model passa por _isModelAssignable()
antes de tocar em qualquer prop — defesa contra mass-assignment direto pelo
client:
- Chaves começadas com
_(estado interno do framework) são sempre recusadas. - Props públicas com nomes sensíveis (família de credenciais —
password,senha... privilégios —role,is_admin... campos de auditoria —created_by,tenant_id...) são bloqueadas mesmo que o componente as declare comopublic. - Uma prop pública que já contém um objeto (ex: o próprio
MadForm, umModelEloquent) nunca é sobrescrita por um escalar vindo do client. - Props não públicas (privadas/protegidas) não são tocadas diretamente — o valor só é aceito se a chave usar notação de ponto para um
MadFormpúblico (ver seção anterior); caso contrário, é descartado.
Exemplo completo — busca live + salvar lazy
class ProdutoListagem extends MadComponent
{
public string $busca = '';
public string $novoProduto = '';
public array $itens = [];
public function mount(): void
{
$this->itens = Produto::where('ativo', '1')->orderBy('nome')->get()->all();
}
// .live: roda a cada pausa de digitação, sem action explícita
public function updatedBusca(string $value): void
{
$this->itens = Produto::where('ativo', '1')
->when($value !== '', fn ($q) => $q->where('nome', 'like', "%{$value}%"))
->orderBy('nome')->get()->all();
}
// mad:model "lazy" (sem .live) — só vai junto quando este método rodar
public function onAdicionar(): MadResponse
{
if (trim($this->novoProduto) === '') {
return (new MadResponse())->fieldError('novoProduto', 'Informe um nome.');
}
Produto::create(['nome' => $this->novoProduto, 'ativo' => '1']);
$this->novoProduto = '';
$this->itens = Produto::where('ativo', '1')->orderBy('nome')->get()->all();
return (new MadResponse())->toast('Produto adicionado!', 'success');
}
}
<mad-input-field name="busca" mad:model.live placeholder="Buscar produto..." />
<span class="muted">Buscando: @madBind('busca')</span>
<ul>
@foreach ($that->itens as $item)
<li>{{ $item->nome }}</li>
@endforeach
</ul>
<form mad:submit="onAdicionar">
<mad-input-field name="novoProduto" label="Novo produto" />
<mad-btn type="submit" variant="primary">Adicionar</mad-btn>
</form>
NUNCA fazer
MAD__BLADE_COMMENT__5__
<input mad:model="busca">
MAD__BLADE_COMMENT__6__
MAD__BLADE_COMMENT__7__
<input mad:model.live="busca">
MAD__BLADE_COMMENT__8__
<input mad:model="busca">
<mad-btn mad:click="onFiltrar">Buscar</mad-btn>
MAD__BLADE_COMMENT__9__
<input mad-model="busca"> MAD__BLADE_COMMENT__10__
MAD__BLADE_COMMENT__11__
<input mad:model="busca" value="{{ $that->busca }}">
MAD__BLADE_COMMENT__12__
public function updatingNome(string $newValue): string
{
return mb_strtoupper($newValue); // retorno é ignorado — NÃO normaliza nada
}
MAD__BLADE_COMMENT__13__
public function updatedNome(string $newValue): void
{
$this->nome = mb_strtoupper(trim($newValue));
}