Docs›Reatividade›Auto-bind
Reatividade

Auto-bind

Mude state, framework infere ops.

Auto-bind é o que faz a maioria dos seus métodos de MadComponent não precisar retornar nada: quando uma action retorna void (ou qualquer valor que não seja MadResponse), o MadComponentHandler não fica no escuro — ele compara o estado antes e depois da chamada, prop por prop, e infere sozinho as ops parciais mais baratas para atualizar o DOM. O algoritmo completo do diff vive em MadWire por dentro — esta página é o lado prático: como escrever actions que tiram proveito disso, e quando não tirar.

Regra prática

Toda action que NAO retorna MadResponse (void, ou qualquer outro tipo) tem o
estado da instancia comparado ANTES vs DEPOIS da chamada, prop publica por
prop publica. Para cada uma que mudou:

  ESCALAR (string/int/float/bool)      -> bind (spans @madBind) + val, se
                                           houver <input name="prop"> no form
  ARRAY registrado como autocomplete   -> reload_completion
                                           (precisa data-mad-autocomplete="prop")
  ARRAY sem registro                   -> full re-render (fallback seguro)
  MadForm (public MadForm $form;)      -> diff granular por bucket: fields,
                                           hidden, readonly, disabled, items
  OBJETO (Model Eloquent, qualquer um) -> full re-render, sempre

"Full re-render" nao e' um bug nem uma falha do auto-bind — e' o fallback
padrao e seguro sempre que o framework nao sabe como expressar a mudanca
como um patch parcial. O HTML nunca fica dessincronizado do state real.

Escalares — bind automático + val quando há campo

Mudar uma prop escalar que corresponde a um name de campo do formulário gera um op val (atualiza o <input>) além do bind sempre gerado para qualquer escalar (atualiza spans @madBind espalhados pela tela). O registro de quem é campo de formulário acontece sozinho, durante o render — cada mad-*-field chama MadFormRegistry::register().

// "pessoa_id" e' o <select> que disparou a action (via mad:change, ver
// Diretivas mad-*). "nome", "email", "telefone" sao <mad-input-field>
// cujo atributo name registrou cada um como tipo 'val' no MadVarRegistry.
public function onPessoaChange(int $pessoaId): void
{
    $pessoa = Pessoa::find($pessoaId);

    $this->form->set('nome',     $pessoa->nome     ?? '');
    $this->form->set('email',    $pessoa->email    ?? '');
    $this->form->set('telefone', $pessoa->telefone ?? '');
}
// Resultado, numa unica resposta parcial, sem voce montar nada na mao:
//   3x op 'val'  (<input name="nome">, name="email", name="telefone")
//   3x op 'bind' (qualquer @madBind('nome') / @madBind('email') / etc
//                 espalhado pela tela, se existir)
A op bind também alimenta o Alpine

Escopada ao wrapper, a op bind atinge dois alvos: todo [data-mad-bind="prop"] (os spans de @madBind) e o state Alpine de qualquer [x-data] do wrapper que contenha a prop — os scopes criados por @madWire(['prop']). Resultado: mad-text, mad-if, mad-show e mad-bind:class amarrados àquela prop re-renderizam sozinhos, sem POST novo e sem full re-render. O valor é coagido ao tipo que já está no state Alpine (number / boolean / string), então uma expressão como mad-text="contador > 5 ? 'Muito' : 'Pouco'" continua comparando número com número. Detalhes em Diretivas mad-*.

form->set() vs MadResponse->val()

SituaçãoUsar
Setar valor de um campo do MadForm$this->form->set('campo', $valor)
Setar valor com condição (só se o campo estiver vazio)MadResponse->val('[name="campo"]', $valor, true)
Setar valor em elemento fora de qualquer MadFormMadResponse->val('#meu-elemento', $valor)
// CERTO — void, auto-bind cuida do resto:
public function onTipoChange(string $tipo): void
{
    $this->form->set('descricao', TipoProduto::find($tipo)?->descricao ?? '');
}

// ERRADO — MadResponse->val() so' para fazer o que form->set() ja' faz sozinho:
public function onTipoChange(string $tipo): MadResponse
{
    $desc = TipoProduto::find($tipo)?->descricao ?? '';
    return (new MadResponse())->val('[name="descricao"]', $desc);
}

// CERTO — MadResponse->val() quando o alvo NAO e' um campo do form
// (ex: atualizar o value de um <input> fora de qualquer <mad-form>):
return (new MadResponse())->val('#filtro-rapido', $valor);

// CERTO — MadResponse->val() com a flag "so' se vazio" (3o param), que
// form->set() nao tem como expressar:
return (new MadResponse())->val('[name="apelido"]', $sugestao, true);

Referência completa de form->set()/setItems() (assinatura, todos os tipos de coleção, armadilhas) em set() e setItems().

Arrays — autocomplete registrado (reload_completion)

Mudar um array público cujo nome bate com um data-mad-autocomplete gera um op reload_completion automaticamente — útil para listas que alimentam um <datalist>/autocomplete de texto livre (não confundir com combos de MadForm, que usam setItems() e o bucket items, não este caminho).

MAD__BLADE_COMMENT__1__
<mad-input-field name="action" label="Action"
    attrs='data-mad-autocomplete="methods"' />
class RouteForm extends MadComponent
{
    // O NOME da prop PHP precisa ser IGUAL ao valor do data-mad-autocomplete.
    public array $methods = [];

    public function onControllerChange(string $controller): void
    {
        if ($controller === '') {
            return;
        }

        $this->form->set('name', \Illuminate\Support\Str::headline($controller));
        $this->methods = $this->getControllerMethods($controller); // array PHP simples
    }

    // Framework gera, na mesma resposta:
    //   1. op 'val'                — <input name="name"> atualizado
    //   2. op 'reload_completion'  — window.methods reescrito no client
}
O nome da prop PHP precisa bater com o atributo

MadVarRegistry indexa por nome de string — a prop pública precisa se chamar exatamente igual ao valor de data-mad-autocomplete (no exemplo acima, methods dos dois lados). Qualquer divergência de nome silenciosamente faz a prop cair no fallback de full re-render — sem erro, só sem o atalho.

MadForm — diff granular por bucket

Uma prop pública do tipo MadForm não é tratada como "objeto genérico" (que sempre forçaria full re-render) — o diff a desmonta em até cinco buckets independentes (fields, hidden, readonly, disabled, items), cada um virando o tipo de op mais barato possível. Por isso hide(), readonly() e setItems() também funcionam de graça em actions void:

// public MadForm $form; e' ele proprio uma prop publica monitorada pelo
// diff — mas o snapshot ANTES/DEPOIS dele e' comparado bucket a bucket
// (fields/hidden/readonly/disabled/items), nao como um "objeto generico"
// (que cairia em full re-render). Cada metodo abaixo muda UM bucket:
public function onAprovar(): void
{
    $this->form->set('status', 'Aprovado');        // bucket fields -> op 'val'
    $this->form->hide('motivo_rejeicao');           // bucket hidden -> op 'mad_hide'
    $this->form->readonly('codigo');                // bucket readonly -> op 'mad_readonly'
    $this->form->loadOptionsFromModel('proximo_responsavel_id', 'Usuario');
    // bucket items -> op 'reload_combo', tudo na MESMA resposta parcial
}

Mecânica completa de cada bucket (e os tipos de op que cada um gera) em MadWire por dentro.

Field-list — auto-bind por linha (sufixo [])

Campos terminados em [] são reconhecidos pelo MadForm como pertencentes a um field-list — as ops resultantes são escopadas à linha de origem do evento, não ao form inteiro. Só funciona dentro de actions disparadas pelo atributo on-change de uma <mad-field-list-column> (internamente, mad:fl-change) — o JS captura a row antes do POST.

<mad-field-list name="itens" addable removable>
    <mad-field-list-column field="produto_id" label="Produto"
        type="dbcombo" model="Produto" display="nome"
        on-change="onChangeProduto" />
    <mad-field-list-column field="valor" label="Valor unitário" type="money" />
    <mad-field-list-column field="unidade" label="Unidade" attrs="readonly" />
</mad-field-list>
// on-change SO' dispara a action via "mad:fl-change" — o JS captura
// row = el.closest('.mad-fl-row') ANTES do POST, entao form->set('campo[]', $v)
// sabe exatamente qual linha patchar, sem afetar as outras.
public function onChangeProduto($value): void
{
    $produto = Produto::find($value);
    if (!$produto) {
        return;
    }

    $this->form->set('valor[]', $produto->preco);     // op fl_val, so' a row de origem
    $this->form->set('unidade[]', $produto->unidade); // op fl_val, so' a row de origem
}
form->set('campo[]', $v)                 -> fl_val    escopo: row de origem
form->setItems('campo[]', $items)        -> fl_combo   escopo: row de origem
form->loadOptionsFromModel('campo[]',..) -> fl_combo   escopo: row de origem
form->setRows('nome_do_field_list', $r)  -> fl_rows    escopo: field-list inteiro

Detalhes completos (roteamento por row, setRows(), quando preferir FieldListColumn::setValue()) em set() e setItems() — Field-list.

Quando usar void vs MadResponse

SituaçãoRetorno
Só mudar estado (props, form->set(), hide(), setItems()...)void — handler gera as ops sozinho
Precisa de UX explícito (toast, abrir/fechar drawer-modal, redirect)MadResponse
Mudar estado e pedir UX explícito na mesma actionMadResponse — as ops do diff são mescladas automaticamente
// Os dois mundos se mesclam SEMPRE na mesma resposta — nao e' "ou um ou
// outro": ops explicitas de um MadResponse e ops geradas pelo diff de
// auto-bind sao concatenadas automaticamente pelo MadComponentHandler.
public function onAprovar(): MadResponse
{
    $this->form->set('status', 'Aprovado');        // auto: op 'val'
    $this->acoesDisponiveis = $this->getAcoes();    // auto: op 'reload_completion'
                                                     // (se registrado) ou full render

    return MadToast::success('Pedido aprovado!');   // explicito: op 'toast'
    // Resposta final: toast + val + reload_completion, tudo junto.
}

Catálogo completo do que um MadResponse pode fazer além de mesclar com o auto-bind em MadResponse — todas as ops.

Arrays e objetos sem registro = full re-render (e por que isso é seguro)

Arrays genéricos (sem data-mad-autocomplete correspondente) e qualquer prop pública que guarde um objeto (um Model Eloquent, por exemplo) sempre disparam re-render completo do componente ao mudar — isso não é uma falha do auto-bind, é o fallback padrão: o framework prefere reconstruir o HTML inteiro a arriscar um patch parcial incorreto para um shape de dado que não sabe diffar com segurança. Mais sobre quando isso acontece e como evitar (quando vale a pena) em Render parcial — Degrau 4.

forceFullRender() — opt-out manual

Às vezes uma mudança é "tecnicamente" um escalar simples, mas troca blocos inteiros da view (não só um valor) — nesse caso o patch parcial que o auto-bind geraria deixaria a tela inconsistente. $this->forceFullRender() força o MadComponentHandler a emitir HTML completo no fim da action atual, ignorando a otimização:

class ProdutoForm extends MadComponent
{
    public string $modoVisualizacao = 'lista'; // 'lista' | 'cards'

    // Trocar $modoVisualizacao e' um escalar comum -> o diff so' geraria
    // bind/val, mas a troca de modo muda BLOCOS INTEIROS da view (a
    // estrutura do HTML muda, nao so' um valor). Force full render aqui:
    public function onTrocarModo(string $modo): void
    {
        $this->modoVisualizacao = $modo;
        $this->forceFullRender();
    }
}

NUNCA fazer

// ERRADO — script() para fazer o que mudar o state ja' resolveria sozinho:
$response->script("document.querySelector('[name=\"nome\"]').value = '{$nome}'");
$response->script('window.methods = ' . json_encode($methods));

// CERTO — mude o state e deixe o auto-bind gerar a op certa:
$this->form->set('nome', $nome);
$this->methods = $methods; // se registrado, vira reload_completion sozinho

// ERRADO — MadResponse->val() quando form->set() ja' resolve:
$response->val('[name="nome"]', $nome);

// CERTO:
$this->form->set('nome', $nome);

// ERRADO — esperar auto-bind num array generico nao registrado:
public array $itensFiltrados = []; // sem data-mad-autocomplete em lugar algum
// ... mudar $this->itensFiltrados sempre dispara full re-render, mesmo void.

// CERTO — ou registre como autocomplete (se for o caso), ou aceite o full
// re-render (e' o comportamento padrao e seguro), ou peca um patch
// explicito via MadResponse->html()/manageRow() (ver Render parcial).

Próximos passos