Docs›Componentes (Admin)›mad-db-blocks
Componentes (Admin)

mad-db-blocks

Coleção 1:N vinculada a parent com CRUD inline.

Componente generico de lista-pivot com CRUD inline: chips, shares, links, attachments — qualquer coleção vinculada a um registro pai via pivot, com add/remove/update sem full re-render.

O visual de cada linha é delegado a um Blade partial (row-view) fornecido pelo consumidor. O form de adição é declarado como filho via <mad-db-blocks-form> e disparado por um botão "+ Adicionar" em popover/modal/drawer.

IMPORTANTE: o controller host DEVE adicionar use \Mad\Form\MadDbBlocksTrait;. Os metodos blockAdd, blockRemove, blockUpdate sao roteados contra o MadComponent host.

Props

Prop Tipo Default Descricao
name string '' Identificador do bloco (obrigatorio, usado em IDs DOM)
mode string pivot pivot = coleção 1:N vinculada a um parent. flat = lista master independente (sem FK)
pivot-model string '' Classe do model Eloquent (obrigatorio). Em mode=pivot aponta pra tabela pivot; em mode=flat aponta pro model master
database string MAIN_DATABASE Conexao
foreign-key string '' Coluna FK do registro pai na pivot. Obrigatorio em mode=pivot, ignorado em mode=flat
record-id int auto ID do registro pai. Auto-resolve via MadRenderContext::getComponent()->registroId. Ignorado em mode=flat
row-view string '' Path do Blade partial que renderiza UMA linha (obrigatorio)
order-by string 'id' Coluna de ordenacao
order-dir string 'asc' asc ou desc
layout string 'inline' inline (chips, quebra linha) ou stack (linhas 100% largura)
add-mode string 'popover' Onde o form inline abre: popover, modal, drawer
add-label string '+ Adicionar' Texto do botao
add-icon string 'plus' Aceito pelo componente, mas o botao padrao atual nao renderiza icone — so add-label aparece
add-variant string 'ghost' Aceito pelo componente, mas o botao padrao atual nao aplica classe de variant — visual fixo (mad-db-blocks-add)
form-position string 'below' Onde o form aparece no modo add-mode="inline": below ou above
submit-label string 'Salvar' Texto do botao de submit do form do item
submit-icon string 'check' Icone Lucide do botao de submit
hide-add-button bool false Esconder botao de adicionar. Forcado a true quando add-mode="inline"
no-list bool false Modo "so adicionar": nao pre-renderiza nem re-renderiza a lista (a exibicao dos itens fica com outro componente — ex.: a tree de pastas do GED)
editable bool false Permite editar o item no lugar, reusando o mesmo form do adder ja preenchido
edit-fields array [] Campos liberados para a edicao in-place
preset-vars array [] Valores livres injetados no template do item — chegam ao row-view como $vars (usado pelos presets <mad-comments> / <mad-attachments>)
confirm-remove bool|string false Confirmacao antes de remover. Opt-in: pelado usa a mensagem padrao (Remover este item?), com valor usa a sua. Chega ao row-view como $vars['confirmRemove']; os row-views do framework emitem data-mad-confirm, interceptado pelo wire antes da acao
empty-text string 'Nenhum item' Texto quando lista vazia
on-add string '' Metodo do host chamado dentro da transacao, antes do save() do novo pivot
on-remove string '' Metodo do host chamado dentro da transacao, antes do delete()
on-update string '' Metodo do host chamado dentro da transacao, antes do save() do update de campo
on-after-add string '' Metodo do host chamado apos o commit de on-add — recebe ($pivot, MadResponse $resp), use pra acrescentar ops no response (ex: operacoes que precisam do commit ja aplicado, como manageRow)
on-after-remove string '' Idem on-after-add, apos o commit da remocao
filters array [] Filtros adicionais na query (sintaxe dbcombo)
class string '' Classes CSS extras

Upload por item (opcional)

Quando file-field e declarado, o blockAdd grava o(s) arquivo(s) desse campo no disco de uploads e preenche path/metadados no proprio item. Multi-arquivo = N itens.

Prop Tipo Default Descricao
file-field string '' Nome do campo de arquivo no form do item — liga o upload
folder string 'uploads' Pasta de destino no disco de uploads
path-column string '' Coluna do item que recebe o path do arquivo
file-name string 'prefix' Estrategia de nome do arquivo gravado
original-name-column string '' Coluna que recebe o nome original enviado
size-column string '' Coluna que recebe o tamanho em bytes
mime-column string '' Coluna que recebe o mime-type
disk-column string '' Coluna que recebe o disco usado

Modos — pivot vs flat

O componente opera em dois modos distintos, controlados pelo atributo mode:

mode="pivot" (default) — coleção vinculada a um parent

Caso classico: tags/permissoes/links/attachments de um registro especifico. Ha sempre um record-id (do parent) e um foreign-key (coluna na pivot que referencia o parent). Ao adicionar um item, o componente automaticamente seta $pivot->{foreignKey} = recordId.

<mad-db-blocks name="tags"
    pivot-model="GedDocumentTag"
    foreign-key="document_id"
    :record-id="$docId"
    row-view="components.partials.tag-chip"
    on-add="onTagCreate">
    ...
</mad-db-blocks>

mode="flat" — lista master independente

Para listas de registros master que nao tem parent: tags do sistema, categorias globais, etiquetas, qualquer collection de "master data" com CRUD inline. Nao ha foreign-key nem record-id — o componente opera diretamente sobre o model.

Nesse modo, pivot-model aponta pro model master (nao pra uma pivot real — o nome do atributo e mantido por compat retroativa).

<mad-db-blocks name="sidebar-tags"
    mode="flat"
    pivot-model="GedTag"
    row-view="ged.partials.sidebar-tag-row"
    on-add="onSidebarTagCreate"
    on-remove="onSidebarTagRemove">

    <mad-db-blocks-form>
        <mad-input-field name="name" label="Nome" required />
        <mad-color-field name="color" label="Cor" />
    </mad-db-blocks-form>
</mad-db-blocks>

Hook on-add em flat mode recebe o proprio record master (nao uma pivot):

public function onSidebarTagCreate(GedTag $tag): void
{
    $tag->created_by = session('userid');
    if (empty($tag->color)) $tag->color = '#a78bfa';
}

Hook on-remove em flat mode deleta o record master. Se ha pivots que o referenciam, limpe-as no hook:

public function onSidebarTagRemove(GedTag $tag): void
{
    GedDocumentTag::where('tag_id', '=', (int) $tag->id)->delete();
}

Quando usar cada modo

Cenario Usar
Tags/labels de um registro (doc, pedido, projeto) mode="pivot"
Compartilhamento (users/groups permissionados num registro) mode="pivot"
Anexos de uma mensagem/tarefa mode="pivot"
Cadastro de tags/categorias do sistema (tela de config) mode="flat"
Lista lateral de filtros dinamicos (ex: sidebar com tags master) mode="flat"
Qualquer CRUD inline de master data sem parent mode="flat"

Slot <mad-db-blocks-form> — form de criacao inline

Conteudo livre — qualquer componente MAD. Filhos viram o body do form disparado pelo botao "+ Adicionar". Os campos sao enviados via mad_model[...] e o framework preenche $this->form do host antes de chamar blockAdd.

Contrato do row-view Blade partial

Recebe:

  • $item — model Eloquent da pivot (relacionamentos disponiveis: $item->familia->nome)
  • $state — token encriptado (usar em mad:click/mad:change, nunca escrever o atributo compilado data-mad-click/data-mad-change a mao)
  • $name — nome do bloco (pra IDs DOM unicos)

blockAdd/blockRemove/blockUpdate sempre re-renderizam a lista inteira (substituem o innerHTML de #mad-db-blocks-list-{name}, o <span> que envolve todas as linhas) — nao existe patch por linha individual. Por isso um id="block-row-{$name}-{$item->id}" no elemento raiz do partial nao e exigido pelo framework para o re-render funcionar; mantenha-o mesmo assim por boa pratica (debug, CSS de transicao, scroll-into-view custom), so nao conte com ele pra um update parcial.

Acoes dentro da linha usam os primitivos do trait:

  • mad:click="blockRemove({{ $item->id }},'{{ $state }}')" — remove 1 registro
  • blockUpdate(int $id, string $field, mixed $value, string $state) — update 1 coluna, mas nao dá pra chamar direto via mad:change: mad-livewire.js sempre anexa o valor do elemento como o último argumento posicional (params.push(el.value)), e aqui $value precisa cair na 3ª posicao (antes de $state, que é a 4ª). A solucao é expor um método-ponte no host que recebe os args na ordem que o mad:change realmente envia (id, field, state, novoValor) e repassa pro trait na ordem certa — ver exemplo abaixo (onRoleChange). Nunca tente passar this.value literal dentro do atributo: o parser de argumentos do MadWire roda JSON.parse() no conteúdo entre parênteses, não avalia JS.

Exemplo 1 — Compartilhamento (lista de users + role editavel)

View

@php
    $roles = ['manage' => 'Gerenciar', 'edit' => 'Editar', 'view' => 'Visualizar'];
@endphp

<mad-db-blocks name="shares"
    pivot-model="GedDocumentShare"
    database="communication"
    foreign-key="document_id"
    row-view="ged.share-row"
    order-by="created_at"
    add-mode="modal"
    add-label="+ Compartilhar"
    empty-text="Nenhum compartilhamento"
    on-add="onShareAdd">

    <mad-db-blocks-form>
        <mad-dbseek-field name="principal_id" label="Usuario"
            model="SystemUsers" display="name" required />
        <mad-select-field name="role" label="Permissao" :options="$roles" required />
    </mad-db-blocks-form>
</mad-db-blocks>

Partial app/resources/views/ged/share-row.blade.php

@php
    $roles = ['manage' => 'Gerenciar', 'edit' => 'Editar', 'view' => 'Visualizar'];
@endphp
<div class="mad-db-blocks-row" id="block-row-{{ $name }}-{{ $item->id }}">
    <mad-avatar :name="$item->principal->name" size="sm" />
    <div style="flex:1;">
        <div>{{ $item->principal->name }}</div>
        <div class="muted">{{ $item->origem }}</div>
    </div>
    <select mad:change="onRoleChange({{ $item->id }},'role','{{ $state }}')">
        @foreach($roles as $v => $l)
            <option value="{{ $v }}" @selected($item->role === $v)>{{ $l }}</option>
        @endforeach
    </select>
    <button mad:click="blockRemove({{ $item->id }},'{{ $state }}')">×</button>
</div>

Controller

use Mad\Component\MadComponent;
use Mad\Form\MadDbBlocksTrait;
use Mad\Http\MadResponse;

class GedDocumentDetail extends MadComponent
{
    use MadDbBlocksTrait;

    public ?int $registroId = null;

    /**
     * Hook on-add — preenche campos que nao vem do form
     */
    public function onShareAdd(GedDocumentShare $share): void
    {
        $share->created_by = session('userid');
        $share->created_at = date('Y-m-d H:i:s');
    }

    // Ponte pro trait: mad:change entrega (id, field, state, valor) — nessa
    // ordem, porque "valor" vem sempre por ultimo (auto-append do JS) — e
    // repassa pro blockUpdate() do trait na ordem que ELE espera.
    public function onRoleChange(int $id, string $field, string $state, string $value): MadResponse
    {
        return $this->blockUpdate($id, $field, $value, $state);
    }
}

Quando nao ha campos pro usuario digitar, o form pode ter apenas configuracao minima e o hook on-add gera o token:

<mad-db-blocks name="links"
    pivot-model="GedDocumentShareLink"
    database="communication"
    foreign-key="document_id"
    row-view="ged.link-row"
    add-mode="popover"
    add-label="+ Criar novo link"
    on-add="onLinkCreate">

    <mad-db-blocks-form>
        <mad-select-field name="expires_in" label="Expira em"
            :options="['1d' => '1 dia', '7d' => '7 dias', '30d' => '30 dias', null => 'Nunca']" />
    </mad-db-blocks-form>
</mad-db-blocks>
public function onLinkCreate(GedDocumentShareLink $link): void
{
    $link->token = bin2hex(random_bytes(16));
    $link->created_by = session('userid');
    if ($link->expires_in) {
        $link->expires_at = date('Y-m-d H:i:s', strtotime('+' . $link->expires_in));
    }
}

Exemplo 3 — Attach de items existentes (form com dbcombo/dbseek)

Para anexar items de uma tabela mestre (ex: vincular produtos a um pedido), declare um <mad-db-blocks-form> com <mad-dbcombo-field> ou <mad-dbseek-field> apontando para o model master:

<mad-db-blocks name="produtos"
    pivot-model="PedidoProduto"
    foreign-key="pedido_id"
    row-view="pedido.produto-row"
    add-mode="modal"
    add-label="+ Adicionar produto">

    <mad-db-blocks-form>
        <mad-dbseek-field name="produto_id" label="Produto"
            model="Produto" display="nome" required />
    </mad-db-blocks-form>
</mad-db-blocks>

O form preenche $pivot->produto_id automaticamente; o trait seta a FK a partir do record-id do parent.

Hooks — convencao de retorno

Todos os hooks (on-add, on-remove, on-update) seguem a mesma convencao:

Retorno Trait faz
void / null / true Segue fluxo: persiste + re-render
false Aborta, rollback, toast de erro
MadResponse Retorna diretamente (controle total — permite save multi-tabela, redirect, etc)
// Hook minimo
public function onShareAdd(GedDocumentShare $s): void
{
    $s->created_by = session('userid');
}

// Hook com validacao
public function onShareAdd(GedDocumentShare $s): bool|MadResponse
{
    if ($s->principal_id == session('userid')) {
        return MadMessage::error('Erro', 'Nao pode compartilhar consigo mesmo');
    }
    $s->created_by = session('userid');
    return true;
}

// Hook com controle total
public function onShareAdd(GedDocumentShare $s): MadResponse
{
    // faz tudo manualmente — save em multiplas tabelas, notificar, etc
    $s->created_by = session('userid');
    $s->save();
    (new NotificationService())->notify($s->principal_id, 'compartilhado');
    return (new MadResponse())
        ->toast('Compartilhado e notificado!', 'success')
        ->closeModal('db-blocks-shares-modal');
}

Hook pos-commit (on-after-add / on-after-remove)

on-add/on-remove/on-update rodam dentro da DB::transaction() — se precisar de algo que so pode acontecer depois do commit (ex: manageRow, que abre uma conexao PDO propria), use on-after-add/on-after-remove. Recebem o pivot ja persistido + o MadResponse que sera devolvido, pra acrescentar ops:

<mad-db-blocks name="shares" pivot-model="GedDocumentShare"
    foreign-key="document_id" row-view="ged.share-row"
    on-add="onShareAdd" on-after-add="onShareAdded">
    ...
</mad-db-blocks>
public function onShareAdd(GedDocumentShare $s): void
{
    $s->created_by = session('userid');
}

public function onShareAdded(GedDocumentShare $s, MadResponse $resp): void
{
    // commit ja aplicado — seguro pra side-effects que abrem outra conexao
    $resp->toast('Compartilhamento salvo e processado!', 'success');
}

Caso especial — tags coloridas com chips

Para tags (padrao chip colorido), use o partial reutilizavel components.partials.tag-chip como row-view. O form cria a tag master via on-add:

<mad-db-blocks name="tags"
    pivot-model="GedDocumentTag" database="communication"
    foreign-key="document_id" :record-id="$docId"
    row-view="components.partials.tag-chip"
    add-mode="popover" add-label="+ Tag"
    on-add="onTagCreate">

    <mad-db-blocks-form>
        <mad-input-field name="name" label="Nome" required />
        <mad-color-field name="color" label="Cor" />
    </mad-db-blocks-form>
</mad-db-blocks>

No controller host, o on-add cria o master GedTag a partir do form e seta $pivot->tag_id:

public function onTagCreate(GedDocumentTag $pivot): void
{
    $data = $_POST['mad_model'] ?? [];
    $tag = new GedTag();
    $tag->name = trim($data['name'] ?? '');
    $tag->color = $data['color'] ?? '#a78bfa';
    $tag->created_by = session('userid');
    $tag->save();
    $pivot->tag_id = $tag->id;
}

NUNCA fazer

{{-- ERRADO: usar componente sem o trait no controller — "Metodo nao permitido: blockAdd" --}}
class MeuForm extends MadComponent { /* sem use MadDbBlocksTrait */ }

{{-- CERTO --}}
class MeuForm extends MadComponent {
    use \Mad\Form\MadDbBlocksTrait;
}

{{-- EVITAR: row-view sem id no elemento raiz — dificulta debug/CSS, mesmo sem ser exigido pelo framework --}}
<div class="row">{{ $item->nome }}</div>

{{-- PREFERIR: id estavel por boa pratica (nao usado pelo blockUpdate, que sempre re-renderiza a lista) --}}
<div class="row" id="block-row-{{ $name }}-{{ $item->id }}">{{ $item->nome }}</div>

{{-- ERRADO: recarregar tudo apos add/remove/update --}}
public function onShareAdd(...) {
    $this->loadData(); // full re-render
}

{{-- CERTO: deixar o trait cuidar do re-render parcial --}}
public function onShareAdd(GedDocumentShare $s): void
{
    $s->created_by = session('userid');
    // trait faz save() + renderList + html() automaticamente
}

{{-- ERRADO: prefixar metodo do trait com _ — handler do framework bloqueia --}}
public function _blockBeforeAdd() { ... }

{{-- CERTO: usar on-add no Blade + metodo publico sem prefixo _ --}}
on-add="onShareAdd"
public function onShareAdd(...) { ... }

{{-- ERRADO: montar HTML da lista manualmente com @foreach no host view --}}
@foreach($shares as $s)
    <div>...</div>
@endforeach

{{-- CERTO: usar db-blocks com row-view --}}
<mad-db-blocks name="shares" pivot-model="..." row-view="ged.share-row" />