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)
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ção | Usar |
|---|---|
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 MadForm | MadResponse->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
}
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ção | Retorno |
|---|---|
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 action | MadResponse — 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).