Docs›Componentes (Admin)›mad-field-list
Componentes (Admin)

mad-field-list

Tabela editável inline dentro de form.

Linhas dinâmicas inline para mestre-detalhe simples (cada linha edita seus campos direto na tabela, sem abrir drawer/modal). Auto-load + auto-save quando declarado com model + foreign-key — mesmo mecanismo do <mad-detail-form>, só que com edição inline em vez de sub-form.

Quando usar: itens de pedido, parcelas, anexos curtos, telefones, endereços — qualquer coleção 1:N com poucas colunas onde edição inline acelera a UX.

Quando NÃO usar: itens com muitas colunas, validação complexa por linha ou ações compostas → <mad-detail-form> (abre um sub-form em drawer/modal/spa por linha).

Quick start

Blade

<mad-field-list name="itens" model="PedidoItem" foreign-key="pedido_id"
    addable removable add-label="Adicionar item">

    <mad-field-list-column field="produto_id" label="Produto" type="dbcombo"
        model="Produto" display="nome" required
        on-change="onChangeProduto" />

    <mad-field-list-column field="quantidade" label="Qtd." type="numeric"
        width="90px" min="0.01" default="1" required />

    <mad-field-list-column field="valor" label="Valor" type="money"
        width="120px" prefix="R$" required />

    <mad-field-list-column field="valor_total" label="Total" type="money"
        readonly compute="{quantidade} * {valor}" sum />
</mad-field-list>

Controller (auto-load + auto-save)

use Illuminate\Support\Facades\DB;

public function onEdit(int $id): void
{
    $pedido = Pedido::findOrFail($id);   // leitura não precisa de transação
    $this->registroId = (int) $pedido->id;
    $this->form->fill($pedido);          // auto-load das rows acontece aqui
}

public function onSave(): MadResponse
{
    try {
        $data = $this->form->getData();
        $this->form->validate(Pedido::rules($this->registroId));

        DB::transaction(function () {
            $pedido = $this->registroId ? Pedido::findOrFail($this->registroId) : new Pedido();
            $this->form->save($pedido);   // auto-save das rows (smart-sync)
        });

        return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
    } catch (MadValidationException $e) {
        return $e->asModal();
    } catch (Throwable $e) {
        return MadMessage::error('Erro', $e->getMessage());
    }
}

Atributos do <mad-field-list>

Attr Tipo Default Descrição
name string — Nome do field-list (obrigatório; acessado via $form->getFieldList($name))
model string — Classe do model Eloquent dos itens — habilita auto-load/save
foreign-key string — Coluna FK no model do filho (ex: pedido_id)
database string MAIN_DATABASE Conexão do model filho
addable bool false Botão "+ adicionar linha" no rodapé
removable bool false Botão "x" em cada linha
sortable bool false Drag-and-drop pra reordenar linhas (handle à esquerda)
add-label string "Adicionar linha" Texto do botão de adicionar
max-rows int 0 Linhas máximas (0 = ilimitado; desabilita o botão de adicionar ao atingir)
label string '' Label acima da tabela
hint string '' Texto de ajuda
error string '' Mensagem de erro
class string '' Classes CSS extras
on-add string '' Método PHP chamado (fire-and-forget) ao adicionar uma linha
on-remove string '' Método PHP chamado ao remover uma linha
on-totalize string '' Método PHP chamado quando os totais (sum/count) mudam (debounced)

Não existem min-rows nem default-rows no componente atual — linhas mínimas/obrigatórias são responsabilidade da validação ($this->form->validate(...)), não do field-list.

<mad-field-list-column> — coluna

Atributos comuns

Attr Descrição
field Nome da coluna no model do filho (obrigatório)
label Cabeçalho
type ver tabela de tipos abaixo
width Largura CSS fixa ('120px', '30%', '1fr')
placeholder Placeholder do input
default Valor pré-preenchido ao adicionar linha nova
required / readonly / disabled Flags booleanas
attrs Atributos HTML extras (ex: data-mad-autocomplete="...")

attrs só é aplicado em colunas que renderizam um <input> simples — text/number/email/tel (e qualquer type não tratado explicitamente). Em money, numeric, discount, combo_input, date, select, dbcombo, checkbox, toggle, radio, textarea, file, multifile e files o atributo é ignorado pelo template (cada um desses tipos renderiza markup próprio, sem passthrough de attrs).

Tipos (type=)

type= Quando usar Props extras
text (default) Texto simples placeholder, default
number Inteiro/decimal HTML5 nativo min, max, step
numeric Decimal pt-BR mascarado decimals, min, max
money Moeda decimals, prefix (ex: R$), min, max
date Data com datepicker min, max
discount Desconto: valor + select %/R$ type-field (default {field}_tipo), decimals
combo_input Número + select inline (genérico) options, type-field, decimals
select Select estático options (mapa 'A' => 'Ativo')
dbcombo Select de banco (auto-load + cascata) model, display, key-field, order-by, where, depends-on, depends-column, database (conexão do model de lookup, se diferente da padrão)
hidden Campo escondido (não renderiza, só mapeia) default
email / tel Validação HTML5 placeholder
checkbox / toggle Booleano ('1'/'0') —
radio 1-de-N options
textarea Texto longo placeholder
file Upload único por linha accept, max-size, storage, folder, file-name, name-column
multifile Upload múltiplo por linha (mesma coluna) idem file
files Upload múltiplo via modal → tabela neto (1 linha por arquivo) model, foreign-key, path-column, name-column, storage, folder
<!-- dbcombo dentro de field-list -->
<mad-field-list-column field="produto_id" label="Produto" type="dbcombo"
    model="Produto" display="nome" required />

<!-- money com soma no rodapé -->
<mad-field-list-column field="subtotal" label="Subtotal" type="money"
    decimals="2" sum />

<!-- select estático inline -->
<mad-field-list-column field="status" label="Status" type="select"
    :options="['A' => 'Ativo', 'I' => 'Inativo']" />

Outros atributos (kebab-case → camelCase)

Attr Blade Método interno Uso
type-field typeField Coluna companheira de discount/combo_input (tipo %/R$)
key-field keyField PK do model em dbcombo (default id)
order-by orderBy Ordenação das options em dbcombo
on-change onChange Método PHP (ou expressão Alpine) disparado no change da célula
depends-on dependsOn Campo da própria linha que dispara reload em cascata
depends-column dependsColumn Coluna do model filho a filtrar na cascata (default = depends-on)
where — Filtro estático nas options: 'ativo=1|tipo=P' ou 'preco:>:0'
file-name fileName Estratégia do nome no disco: prefix | unique | original | record
name-column nameColumn Coluna do model filho pro nome original do arquivo
foreign-key foreignKey (só type="files") FK do neto apontando pra linha do field-list
path-column pathColumn (só type="files") coluna do neto pro path (disk) ou BLOB (db)
decimals, max-size int Casas decimais / tamanho máximo (KB)
min, max, step float Limites numéricos
sum / count flags Totalizador no rodapé (soma ou contagem)
compute string Fórmula client-side (ver abaixo)
items alias de options :items="[...]" funciona igual a :options="[...]"

Estado condicional POR ROW (disabled-when e amigos) — 5.x

Quatro atributos ligam o estado do controle da célula ao conteúdo da própria linha, sem round-trip no servidor (viram bind Alpine no x-for):

Attr Blade Método interno Efeito
disabled-when disabledWhen Desabilita o controle da célula
readonly-when readonlyWhen readonly (só controles de texto)
required-when requiredWhen required condicional
visible-when visibleWhen x-show na célula inteira

Sintaxe (FieldListColumn::compileCondition): {campo} vira row['campo']. Se a expressão não tiver nenhum {token}, ela passa crua como JS Alpine (row está no escopo) — compatibilidade com quem já escrevia row['x'] === 'y'.

<mad-field-list-column field="field_type" label="Tipo" type="select"
    :options="['text' => 'Texto', 'select' => 'Select']" />

<mad-field-list-column field="options" label="Opções" type="text"
    disabled-when="{field_type} != 'select'" />

<mad-field-list-column field="desconto" label="Desconto" type="money"
    readonly-when="{tipo} == 'F'" required-when="{qtd} > 0 && {ativo}" />

<mad-field-list-column field="obs" label="Obs" type="text"
    visible-when="{tipo} == 'S'" />

Cuidado com visible-when: x-show é só CSS — a célula continua no DOM e é SUBMETIDA. Para não gravar o valor, combine com disabled-when (input desabilitado não entra no POST) ou limpe o campo no on-change.

compute — fórmula client-side

<mad-field-list-column field="valor_total" label="Total" type="money"
    readonly compute="{quantidade} * {valor}" sum />

<mad-field-list-column field="liquido" label="Líquido" type="money"
    readonly compute="discount({valor}, {desconto}, {tipo_desconto})" />

Funções disponíveis: operadores + - * /, round(expr, casas), max(...), min(...), abs(...), floor(...), ceil(...), if(cond, then, else) e discount(base, {valor_desc}, {tipo_desc}) (tipo='%' aplica percentual, qualquer outro valor aplica como R$ fixo). {campo} resolve pro valor atual da própria linha (reativo via Alpine x-effect).

sum — totalizador no rodapé

<mad-field-list-column field="valor_total" label="Total" type="money" sum />

Mostra soma total no footer (só aparece quando pelo menos uma coluna tem sum ou count).

on-change — callback PHP por linha

<mad-field-list-column field="produto_id" label="Produto" type="dbcombo"
    model="Produto" display="nome"
    on-change="onChangeProduto" />
public function onChangeProduto($value): void
{
    $p = Produto::find($value);
    if (!$p) return;

    $this->form->set('valor[]', $p->preco);                  // op fl_val (escopo de row)
    $this->form->set('unidade[]', $p->unidade);
    \Mad\Form\FieldListColumn::loadOptions('familia_id', 'Familia', 'id', 'nome'); // op fl_combo
}

form->set('campo[]', ...) afeta apenas a row de origem do change event (o JS captura row = el.closest('.mad-fl-row') antes do POST). Alternativa fluente: \Mad\Form\FieldListColumn::setValue('campo', $valor) e FieldListColumn::loadOptions($target, $model, $key, $display) — ambos retornam MadResponse e podem ser encadeados com ->merge(...).

Ações por linha — <mad-field-list-action>

Botões customizados em cada linha (além do remover padrão), com dois modos: action (mad:click server-side) ou navegação.

<mad-field-list name="itens" model="PedidoItem" foreign-key="pedido_id" removable>
    <mad-field-list-column field="produto_id" label="Produto" type="dbcombo" model="Produto" display="nome" />
    <mad-field-list-column field="quantidade" label="Qtd" type="numeric" />

    <mad-field-list-action method="onDuplicar" icon="copy" title="Duplicar" />
    <mad-field-list-action navigate="ProdutoDetalhe::show({produto_id})" icon="eye" title="Ver produto" />
    <mad-field-list-action method="onZerar" icon="rotate-ccw" variant="danger" confirm="Zerar esta linha?" />
</mad-field-list>
Attr Descrição
method Método mad:click no host (modo action)
navigate Classe ou Classe::metodo({campo}) (modo navegação — placeholders resolvidos da própria row)
icon Ícone Lucide
label / title Texto do botão / tooltip
variant danger | primary | success | warning | info | ghost
confirm Mensagem de confirmação antes de disparar

Auto-load + auto-save (smart-sync)

Quando model + foreign-key estão na tag, o field-list segue o mesmo pipeline do <mad-detail-form> — ver detalhamento completo em detail-form.md:

  1. Load ($this->form->fill($mestre)): monta Model::query()->where('foreign_key', $mestre->id), ordena por id, normaliza as rows e popula $this->form->fields[$name].
  2. Save ($this->form->save($mestre)): no _afterStore, cada row com id é carregada via find($pkVal) ?? new $model() (gera UPDATE), rows sem id geram INSERT, e rows que sumiram do POST são deletadas (whereNotIn('id', $savedIds)).
  3. Rows totalmente vazias (exceto chaves __*) são ignoradas no save.

Hooks PHP

Registrar antes de form->save/form->fill (mesmo contrato do detail-form):

// onLoadDetail — transformar rows ao carregar
$this->form->onLoadDetail('itens', fn($item, $mestre) => [
    'familia_id' => $item->produto_id ? ($item->produto->familia_id ?? '') : '',
]);

// onSaveDetail — transformar/validar ao salvar
$this->form->onSaveDetail('itens', function ($item, $mestre, $row): void {
    $item->valor_total = (float) $item->valor * (float) $item->quantidade;
    $mestre->valor_total += $item->valor_total;
});

Ordem de execução: setar colunas → setar FK → hook → save() da linha.

A versão atual do framework removeu o método detailCriteria() (escopo extra de WHERE no load/delete). Tanto o auto-load/auto-save declarativo quanto o MadFieldListTrait escopam o detail só pela foreign-key — se o filho compartilha tabela com outros tipos e você precisa de um filtro adicional, monte a query manualmente (ver escopo extra em detail-form.md).

Sem model no Blade — manual via Trait

use Mad\Form\MadFieldListTrait;

class MeuForm extends MadComponent
{
    use MadFieldListTrait;

    public function onEdit(int $id): void
    {
        $pedido = Pedido::findOrFail($id);
        $this->registroId = (int) $pedido->id;
        $this->form->fields['itens'] = $this->loadDetailRows('PedidoItem', 'pedido_id', $id);
    }

    public function onSave(): MadResponse
    {
        $data = $this->form->getData();
        \Illuminate\Support\Facades\DB::transaction(function () use ($data) {
            $pedido = $this->registroId ? Pedido::findOrFail($this->registroId) : new Pedido();
            $pedido->save();
            $this->saveDetailItems('PedidoItem', 'pedido_id', $pedido->id, $data->itens);
        });
        return (new MadResponse())->toast('Salvo!', 'success')->closeDrawer();
    }
}

saveDetailItems() faz smart-sync (rows com id → update/insert + delete dos ausentes) ou delete+insert quando as rows não têm coluna id. loadDetailRows() carrega via Eloquent (Model::query()->where($fk, $parentId)) e normaliza as rows prontas para o field-list.

Acesso a rows via PHP

$rows = $this->form->getFieldList('itens');
// [
//   ['id' => 1, 'produto_id' => 5, 'quantidade' => 2, ...],
//   ['id' => '', 'produto_id' => 7, 'quantidade' => 1, ...],  // row nova
// ]

Útil para validações cruzadas, agregações, etc.

Gerar/manipular rows em PHP (FieldListColumn)

use Mad\Form\FieldListColumn;

// Gerar N linhas com valores pré-preenchidos (ex: 12 parcelas)
$this->parcelas = FieldListColumn::buildRows(12, function (int $i) use ($valorParcela, $dataBase) {
    return [
        'parcela'    => $i + 1,
        'valor'      => round($valorParcela, 2),
        'vencimento' => date('Y-m-d', strtotime("+{$i} months +1 month", strtotime($dataBase))),
    ];
});

$this->parcelas = FieldListColumn::clearRows();                       // []
$this->parcelas = FieldListColumn::removeRow($this->parcelas, 2);     // remove a 3ª linha
$this->parcelas = FieldListColumn::updateRow($this->parcelas, 0, ['valor' => 150.00]);

Pra empurrar rows pro client sem re-render completo, use a op parcial:

return FieldListColumn::setRows('parcelas', $this->parcelas);

Gotchas

  • form->set('campo[]', ...) só funciona em actions disparadas por on-change da coluna (mad:fl-change) — não chamar em submit/click global, a op fl_val fica órfã
  • compute é client-side — não confie para validação/storage; recompute no onSaveDetail
  • Não há canal de erro por-célula. dfFieldError() é do <mad-detail-form> (escopo [data-df-fields]); o field-list não tem equivalente por linha. Para erro de item, use error no <mad-field-list> inteiro ou fieldError() num campo master. E cuidado: fieldError('campo') usa o seletor [data-field-error="campo"]:not([data-df-fields] *) — se o field-list tiver coluna homônima a um campo master, o erro do master continua no lugar certo, mas não existe destino dentro da linha
  • Rows totalmente vazias são ignoradas no smart-sync — exceto chaves __*
  • Não chame saveDetailItems() manual quando o Blade tem model+foreign-key — roda 2x (auto-save + manual)
  • FK na row é sempre forçada para $mestre->{pk} — input com nome FK no field-list não tem efeito
  • sum totaliza apenas valores numéricos — strings/dates são ignoradas
  • Mudou colunas? Limpe bootstrap/cache/blade-* / cache de views Blade — o compilador gera PHP estático a partir das <mad-field-list-column>

Ver também: <mad-detail-form> para mestre-detalhe com sub-form completo por linha.