# Sticky Footer Save / Cancel Buttons on Edit Pages

## What It Does

Adds a sticky footer bar at the bottom of the viewport containing the Save and Cancel buttons. The bar stays visible as the user scrolls, so they never need to scroll to the bottom to save.

This is already active on:
- **Inventory > Products > Edit** — `plugins/webkul/inventories/src/Filament/Clusters/Products/Resources/ProductResource/Pages/EditProduct.php`
- **Channel Orders > Edit** — `plugins/webkul/channel-orders/src/Filament/Resources/OrderResource/Pages/EditOrder.php`
- **Purchase Orders > Edit** — `plugins/webkul/purchases/src/Filament/Admin/Clusters/Orders/Resources/OrderResource/Pages/EditOrder.php`

---

## How to Add It

### Step 1 — Add the import

In the page class file (e.g. `EditSomething.php`), add the `Alignment` import:

```php
use Filament\Support\Enums\Alignment;
```

### Step 2 — Add the two static properties to the page class

```php
class EditSomething extends EditRecord
{
    public static bool $formActionsAreSticky = true;

    public static string|Alignment $formActionsAlignment = Alignment::End;

    // ... rest of the class
}
```

That's it. Filament reads these two properties and renders the form action bar as a sticky footer aligned to the right (`Alignment::End`).

---

## Properties Explained

| Property | Type | Effect |
|----------|------|--------|
| `$formActionsAreSticky` | `bool` | `true` = footer sticks to the bottom of the viewport while scrolling |
| `$formActionsAlignment` | `Alignment` | `Alignment::End` = buttons align right. Other options: `Alignment::Start`, `Alignment::Center` |

---

## Example — Minimal Edit Page with Sticky Footer

```php
<?php

namespace App\Filament\Resources\SomeResource\Pages;

use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Alignment;

class EditSomething extends EditRecord
{
    protected static string $resource = SomeResource::class;

    public static bool $formActionsAreSticky = true;

    public static string|Alignment $formActionsAlignment = Alignment::End;
}
```

---

## Notes

- This works on any `EditRecord` or `CreateRecord` page — the properties are defined on Filament's base page class and just need to be set to `true`.
- The footer includes the default Save / Cancel actions. If you have custom `getFormActions()` those are also included.
- No CSS or blade overrides needed — Filament handles the rendering.

---

## Adding a Sticky Footer to a ViewRecord Page (Custom Actions)

`ViewRecord` does **not** have `getFormActions()` built in, so the two-property approach above won't add buttons on its own. The correct approach is to override `content()` and inject a Filament `Actions` schema component directly into the page content — exactly the same internal mechanism EditRecord uses for Save/Cancel.

### What does NOT work (failed attempts — do not repeat)

| Approach | Why it fails |
|---|---|
| `getFooter()` returning a Blade view with `wire:click="mountAction('...')"` | `x-filament::button` strips `wire:click` in some contexts; Alpine wrapper caused silent JS errors |
| `getFooter()` with plain `<button wire:click>` | Buttons rendered but clicks did nothing — root cause unclear, likely Livewire morph boundary |
| `->hidden()` on header actions + `wire:click` in footer | Hidden actions ARE cached but the button trigger still failed in practice |
| `x-data="filamentActionsSchemaComponent()"` + `x-intersect` | `x-intersect` Alpine plugin may not be loaded on View pages, causing silent init failure |
| Keeping ChatterAction in the header, everything else in footer | ChatterAction renders as a floating icon-only button with no context — looks out of place |

### The correct approach: override `content()`

```php
use Filament\Schemas\Components\Actions as ActionsComponent;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment;

class ViewSomething extends BaseViewRecord
{
    public static bool $formActionsAreSticky = true;

    public static string|Alignment $formActionsAlignment = Alignment::End;

    protected function getHeaderActions(): array
    {
        return []; // Clear the header entirely
    }

    public function content(Schema $schema): Schema
    {
        return $schema->components([
            $this->hasInfolist()
                ? $this->getInfolistContentComponent()
                : $this->getFormContentComponent(),
            $this->getRelationManagersContentComponent(),
            ActionsComponent::make([
                // Define each action here as a full Action object
                ChatterAction::make()->setResource(static::$resource),
                $this->makeSomeAction(),
                $this->makeAnotherAction(),
            ])
            ->alignment($this->getFormActionsAlignment())
            ->sticky($this->areFormActionsSticky())
            ->key('view-footer-actions'), // unique key required
        ]);
    }
}
```

### Key rules

- **`$formActionsAreSticky = true` is still required** — it is read by `areFormActionsSticky()` which is passed to the `Actions` component.
- **`->key('...')` is required** — Filament uses this to identify the schema component for action mounting. Make it unique per page.
- **Actions must be fully defined in `content()`** — they are schema-level actions, not header actions. They cannot reference header-mounted actions by name.
- **ChatterAction must go in the footer too** — do not leave it as a lone header action. Without breadcrumbs/heading in the header it renders as a floating icon button with no context.
- **`getHeaderActions()` should return `[]`** — avoids duplication and the floating-button problem.

### ChatterAction in the footer

```php
use Webkul\Chatter\Filament\Actions\ChatterAction;

// Inside ActionsComponent::make([...]):
ChatterAction::make()->setResource(static::$resource),
```

`setResource()` is required — ChatterAction needs to know the resource to load the correct activity log config.

### Actions that are defined elsewhere (e.g. in a trait)

If an action is defined in a trait method like `makeArchiveAction()`, call it directly:

```php
ActionsComponent::make([
    $this->makeArchiveAction(),
])
```

### Actions that were previously header actions (e.g. Print)

Header actions defined in the base class cannot be re-used as schema actions by reference. Copy the action definition into a protected method on the page class:

```php
protected function makePrintAction(): Action
{
    return Action::make('print')
        ->label('Print Labels')
        ->color('gray')
        ->icon('heroicon-o-printer')
        ->schema([...])
        ->action(function (array $data, $record) {
            // PDF generation etc.
        });
}
```

### Real example

`plugins/webkul/inventories/src/Filament/Clusters/Products/Resources/ProductResource/Pages/ViewProduct.php`

---

## Known Issue — Short Pages (Already Fixed Globally)

### Problem

On short pages (content shorter than the viewport height), the sticky bar does **not** fix to the viewport bottom. It just sits below the form content in normal flow.

**Root cause:** Filament's sticky actions use an Alpine.js intersection observer (`x-intersect:leave`) that only fires when the bar scrolls *out of* the viewport. On a short page that never scrolls, this never fires, so `isSticky` stays `false` and the `fi-sticky` CSS class is never applied.

### Fix (already applied globally in `AdminPanelProvider.php`)

A JS snippet in `SCRIPTS_AFTER` forces `isSticky = true` via `Alpine.$data()` on any `.fi-sc-actions[x-data]` element after each page navigation:

```javascript
function fixShortPageSticky() {
    document.querySelectorAll('.fi-sc-actions[x-data]').forEach(function (el) {
        try {
            var data = Alpine.$data(el);
            if (!data.isSticky) {
                data.isSticky = true;
            }
        } catch (e) {}
    });
}
document.addEventListener('DOMContentLoaded', function () { setTimeout(fixShortPageSticky, 200); });
document.addEventListener('livewire:navigated', function () { setTimeout(fixShortPageSticky, 200); });
```

The 200 ms delay lets Alpine finish initialising the component before we write to its data.

**You do not need to add anything extra when applying the sticky footer to new pages** — the global fix covers all pages automatically.
