Docs›Reatividade›Two-way binding
Reatividade

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.

Não confundir com 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

DiretivaQuando sincronizaChama 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();
    }
}
Custo de .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}().

Os hooks 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 elementoValor enviado em mad_model[prop]
texto, número, data, textarea, select simplesel.value direto
<input type="checkbox">"1" marcado / "0" desmarcado
<select multiple>JSON array dos valores selecionados
mad-checkbox-group-fieldJSON array dos valores marcados
mad-radio-field (grupo)valor do <input type="radio"> marcado
mad-checklist-fieldJSON array dos IDs marcados
Inputs dentro de field-list / detail-form não entram aqui

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 como public.
  • Uma prop pública que já contém um objeto (ex: o próprio MadForm, um Model Eloquent) 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 MadForm pú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));
}

Próximos passos