# Dashing / Loading State Pattern for Calculated Fields

When a Livewire form recalculates values (e.g. line totals, order summary), fields briefly show stale data while the server processes the update. The "dashing" pattern replaces calculated display fields with an animated `—` during that window, giving clear visual feedback.

---

## How It Works

Filament uses Livewire 3 under the hood. Every form interaction that calls `.live()` sends a network request. Livewire provides two directives:

- `wire:loading.remove` — hides the element while a request is in flight
- `wire:loading` — shows the element only while a request is in flight

By wrapping the real value in a `wire:loading.remove` span and pairing it with a `wire:loading` dash span, the UI shows `—` during recalculation and the real value otherwise.

```html
<span wire:loading.remove wire:target="...">£12.50</span>
<span wire:loading wire:target="..." class="animate-pulse text-gray-400" style="display:none">—</span>
```

`style="display:none"` on the dash prevents a brief flash on initial page load before Livewire attaches.

---

## Two Levels of Dashing

### 1. Per-Row Calculated Cells (Repeater line totals)

Each repeater row has a UUID in its Livewire state path (e.g. `data.products.{uuid}.field`). Scoping `wire:target` to only the fields in **this row** means only the row being edited flashes — other rows stay stable.

**Implementation — add this method to the Resource class:**

```php
private static function rowCalcCell(string $value, $component): \Illuminate\Support\HtmlString
{
    // Extract repeater item UUID from the absolute state path.
    // Format: "data.{repeaterKey}.{uuid}.fieldName"
    $parts  = explode('.', $component->getStatePath() ?? '');
    $idx    = array_search('products', $parts); // <-- change 'products' to your repeater key
    $uuid   = ($idx !== false) ? ($parts[$idx + 1] ?? '') : '';

    // Fields whose changes trigger recalculation — adjust to match your repeater fields
    $fields = ['product_qty', 'price_unit', 'taxes', 'discount', '_sku_search', '_name_search'];
    $target = e(implode(',', array_map(
        fn ($f) => "data.products.{$uuid}.{$f}", // <-- change 'products' to your repeater key
        $fields
    )));

    return new \Illuminate\Support\HtmlString(
        '<span wire:loading.remove wire:target="' . $target . '">' . $value . '</span>'
        . '<span wire:loading wire:target="' . $target . '" class="animate-pulse text-gray-400" style="display:none">—</span>'
    );
}
```

**For each calculated display field in the repeater schema:**

The real value must still be stored in a Hidden field (so it saves to DB and is readable via `$get()`). The display uses a `Placeholder` that reads from the Hidden field and wraps it with `rowCalcCell()`.

```php
// Storage — hidden, saves to DB
TextInput::make('price_subtotal')
    ->hidden()
    ->dehydratedWhenHidden(true),

// Display — Placeholder with dashing
Placeholder::make('_price_subtotal_display')
    ->label('Amount')
    ->content(function (Get $get, $component): \Illuminate\Support\HtmlString {
        $symbol = '£'; // or resolve from currency
        return static::rowCalcCell(
            $symbol . number_format((float) ($get('price_subtotal') ?? 0), 2),
            $component,
        );
    }),
```

The `TableColumn` in the repeater header must reference the **Placeholder name**, not the hidden input:

```php
TableColumn::make('_price_subtotal_display')->label('Amount')->width(150),
```

### 2. Summary Totals (Order-level subtotal / tax / total)

Replace any child Livewire summary component with `Placeholder` components directly in the parent form schema. This keeps them in the parent Livewire scope and ensures they always show the correct computed value.

> **Important:** If totals live in a child Livewire component (e.g. `Livewire::make(Summary::class, ...)`), `wire:loading` inside it will NOT flash during parent form updates, because reactive re-renders don't trigger the child's own network requests. Move the display into the parent form as Placeholders instead.

> **Do NOT use `wire:loading` on summary Placeholders.** Unscoped `wire:loading` fires for **any** Livewire request — including the save action — which causes the totals to dash out when the user saves the form. Summary Placeholders should just return the computed value directly; Livewire re-renders them fresh after every request anyway.

```php
Group::make([
    Placeholder::make('_subtotal_row')
        ->label('Untaxed Amount')
        ->inlineLabel()
        ->content(fn (Get $get): string => static::computePOSubtotalDisplay($get)),

    Placeholder::make('_tax_row')
        ->label('Tax')
        ->inlineLabel()
        ->content(fn (Get $get): string => static::computePOTaxDisplay($get)),

    Placeholder::make('_total_row')
        ->label('Total')
        ->inlineLabel()
        ->extraAttributes(['style' => 'border-top:1px solid #e5e7eb;padding-top:8px;margin-top:4px'])
        ->content(fn (Get $get): \Illuminate\Support\HtmlString => static::computePOTotalDisplay($get)),
])
->extraAttributes(['style' => 'margin-left:auto;width:360px']),
```

**Compute methods:**

```php
protected static function computePOSubtotalDisplay(Get $get): string
{
    $lines    = $get('products') ?? []; // your repeater key
    $subtotal = collect($lines)->sum(fn ($l) => (float) ($l['price_subtotal'] ?? 0));
    $currency = Currency::find($get('currency_id'));
    return ($currency?->symbol ?? '£') . number_format($subtotal, 2);
}

protected static function computePOTaxDisplay(Get $get): string
{
    $lines = $get('products') ?? [];
    $tax   = collect($lines)->sum(fn ($l) => (float) ($l['price_tax'] ?? 0));
    $currency = Currency::find($get('currency_id'));
    return ($currency?->symbol ?? '£') . number_format($tax, 2);
}

protected static function computePOTotalDisplay(Get $get): \Illuminate\Support\HtmlString
{
    $lines    = $get('products') ?? [];
    $subtotal = collect($lines)->sum(fn ($l) => (float) ($l['price_subtotal'] ?? 0));
    $tax      = collect($lines)->sum(fn ($l) => (float) ($l['price_tax'] ?? 0));
    $currency = Currency::find($get('currency_id'));
    $symbol   = $currency?->symbol ?? '£';
    return new \Illuminate\Support\HtmlString(
        '<strong style="font-size:1.05em">' . $symbol . number_format($subtotal + $tax, 2) . '</strong>'
    );
}
```

---

## Checklist When Adding to a New Edit Page

1. Identify every calculated field shown to the user (line totals, order totals, tax amounts).
2. For **repeater row fields**: split each into a hidden TextInput (storage) + Placeholder (display). Change the `TableColumn` to reference the Placeholder name.
3. Add `rowCalcCell()` — update the repeater key name (`products`, `orderLines`, etc.) and the trigger field list.
4. For **summary totals**: add Placeholder components directly in the parent form schema (not a child Livewire component). Write compute methods that read from `$get('repeaterKey')`. **Do NOT use `wire:loading` on summary Placeholders** — it fires on save too, causing the totals to dash out incorrectly.
5. No JS needed — entirely Livewire 3 directives + Filament Placeholders.

---

## Reference Implementations

- **Channel Orders**: `plugins/webkul/channel-orders/src/Filament/Resources/OrderResource.php`
  - Repeater key: `orderLines`
  - Trigger fields: `quantity`, `unit_price`, `_sku_search`, `_name_search`
- **Purchase Orders**: `plugins/webkul/purchases/src/Filament/Admin/Clusters/Orders/Resources/OrderResource.php`
  - Repeater key: `products`
  - Trigger fields: `product_qty`, `price_unit`, `taxes`, `discount`, `_sku_search`, `_name_search`
