Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Placeholder Input Laravel Package

wotz/filament-placeholder-input

wotz/filament-placeholder-input adds placeholder text support to Filament form inputs, helping Laravel developers improve form usability by displaying helpful hints inside fields. Lightweight, easy to install, and designed to integrate smoothly with existing Filament applications.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install into an existing Filament app:

    composer require wotz/filament-placeholder-input
    

    The package is now on the v2 line (v2.1.1). Older blog posts, gists, and any v1 README snippets you find will not map cleanly onto v2 — treat the source as the reference. Check the resolved constraint (composer show wotz/filament-placeholder-input) and confirm it matches your Filament major version; a v2 major bump almost certainly tracks a Filament major (v4 Filament\Schemas\Components) rather than shipping parallel branches. Verify before adopting.

  2. Read the source before the README. With packages this small the class itself is the documentation. Start at:

    • composer.json → autoload / extra.laravel (any service provider, config publish, views, assets)
    • src/ → the field class (PlaceholderInput.php or Forms/Components/PlaceholderInput.php) for the public API: how it targets another field, whether it appends/prepends/replaces, which state hooks it uses, and getVariables() — the token-resolution method touched in 2.1.1, which tells you how placeholder variables are declared and evaluated
    • resources/views/ if present, to see whether the interaction is server-side (Livewire round trip) or Alpine-side
    • README.md last, for a copy-pasteable schema snippet
  3. First use case: a RichEditor (or Textarea) plus a small input that inserts reusable snippets/tokens into it. Sketch:

    use Wotz\FilamentPlaceholderInput\Forms\Components\PlaceholderInput;
    
    RichEditor::make('content')->live(),
    
    PlaceholderInput::make('placeholder')
        ->target('content'), // exact method name comes from the source
    

    Register both fields in the same form schema, open the resource create page, type into the placeholder input, and confirm the target field updates in the browser and survives a save. If the second part fails, the package is only doing client-side insertion and you'll need your own mutateFormDataBeforeSave.

  4. Decide quickly whether it fits. If the source is <200 lines and uses afterStateUpdated + $set(), it's a thin convenience wrapper you could reimplement in an hour — worth knowing before you build a workflow on it. Note the package does now have CI (GitHub Actions), Dependabot, and outside contributors (@jyrkidn), so it's maintained better than the star count suggests — but adoption is still low enough that you are the upstream triage.

Implementation Patterns

Targeting a sibling field. The core pattern is a field whose state is not what you persist — it exists to push content into another field's state. Treat it as a controller, not a data column:

PlaceholderInput::make('snippet_picker')
    ->target('content')
    ->dehydrated(false), // if the package doesn't already do this

If dehydrated(false) isn't set by the package and your model has no snippet_picker column, you'll get a "column not found" error on save. Verify with dd($this->form->getState()) in a handleRecordCreation override or via Livewire devtools.

Variables and tokens. getVariables() is the heart of the package's placeholder resolution — it builds the map of tokens available for insertion. Read it before wiring anything up: it tells you whether variables are hardcoded, pulled from config, or exposed as a fluent schema method (->variables([...])). If it's hardcoded, your realistic options are (a) config-driven if a config key exists, or (b) subclass and override. If it's fluent, define the variable set once in a small helper and reuse it across resources so token names stay consistent between the editor and any downstream renderer you build.

Reactive two-way flow. The reliable Filament pattern for cross-field writes is server-side:

PlaceholderInput::make('snippet_picker')
    ->target('content')
    ->live()
    ->afterStateUpdated(function ($state, Set $set) {
        $set('data.content', $state); // or append: $set('data.content', $current . $state)
    }),

Build this yourself if the package's built-in binding proves brittle. The package's value is mostly the UI affordance (a picker, a token list) and the variable resolution; the state plumbing is five lines and you should own it if you depend on it.

Append vs. replace. Most "add content to another field" fields are appending. Confirm the semantics in the source. For an append workflow, keep the current target value in the closure ($get('data.content')) and concatenate, then clear the placeholder input so repeated clicks accumulate rather than duplicate.

Repeaters and Builder. Inside Repeater::make('items')->schema([...]), the target path must be relative to the repeater item (content), not absolute (data.items.0.content). Test with two repeater rows — if row 0's picker writes into row 1's editor, the package is hardcoding data. prefixes and you should avoid it inside repeaters.

Modals and actions. Works the same in Action::make()->form([...]); just remember the target field must be in the same schema. It cannot write into the underlying resource form from a modal action.

Persistence layer. If insertion happens client-side (Alpine), Livewire won't see it. Add a mutateFormDataBeforeSave or beforeSave hook that reads the target field's state and writes it through, and add an integration test that submits the form without touching the target field manually.

Testing. Use Filament's livewire(CreatePost::class)->fillForm([...])->call('create')->assertHasNoFormErrors(), then assert the persisted content contains the inserted text. This is the single highest-value test for this package because the failure mode (state shows in UI, never persists) is silent. Add a second case that exercises a variable-bearing placeholder so the getVariables() path is covered — that's the method that just shipped a bug fix.

Gotchas and Tips

  • Upgrade to 2.1.1 if you're on 2.0.x–2.1.0 and use variables. v2.1.1 fixes operator precedence in getVariables(). Practical symptom in the earlier patch: composite expressions (concatenation combined with ?? / ?: / a comparison) resolve to the wrong token or the wrong default. If your placeholders look correct for single variables but break the moment you combine them, that's this bug. It's a one-line patch release — no migration work.
  • v1 → v2 is a major bump. If you pinned v1, don't assume composer update semantics carry over. Read the v2.0.0 changelog / diff for renamed fluent methods and any Filament 4 namespace move (Filament\Forms\Components\Field → Filament\Schemas\Components\Field). Expect one focused refactor, not a drop-in.
  • Pin exactly, but not out of fear. "wotz/filament-placeholder-input": "2.1.1" rather than ^2.1 is still the right call for a low-adoption package — but the presence of CI, Dependabot, and an external PR author means patch releases are likelier to be small and honest than to be rewrites.
  • Filament v4 namespace churn. Filament 4 moved form components under Filament\Schemas\Components. Check whether the v2 field class extends the schemas Field or still leans on Filament\Forms aliases; the latter works today but generates deprecation noise and will break at the next major.
  • afterStateUpdated collisions. If the package registers its own afterStateUpdated internally and you chain another one, one may overwrite the other depending on whether it merges or replaces closures. Read the field's setUp() to see. If it replaces, extend the class and override rather than chaining.
  • Dehydration is the usual bug. A field designed to feed another field is a poor fit for a database column. Confirm it either calls dehydrated(false) itself or that you do. Symptom: SQLSTATE[42S22]: Column not found on create/update.
  • Fill-time behavior. On edit, the target field arrives pre-filled from the model. Make sure the placeholder input starts empty (->default(null) / no fill()) — otherwise the existing content gets re-appended on first interaction.
  • Validation stays on the target. Rules belong on content, not on the picker. If you validate the picker, users can bypass required-content rules by never touching it.
  • Variable drift between editor and renderer. Whatever getVariables() produces must match what your front end substitutes. If you render stored content elsewhere (Blade, mail, API), extract the variable map into a shared class rather than duplicating the keys — the package's resolution is only half of the contract.
  • Livewire-only state. Anything the package does with Alpine (x-on:click) is invisible to $this->form->getState() until a Livewire request fires. If tests fail but the browser works, add ->live() or a wire:model binding, or force a $refresh.
  • Extension point. Because the package is small, extending the field class and overriding setUp() (adding your own target(), mode(), separator() helpers, or overriding getVariables() for a config-driven token list) is cheaper than forking. Publish nothing; just subclass.
  • Debugging loop. Livewire::dump() on the form state, or ->tap(fn ($field) => logger($field->getStatePath())), tells you the resolved state path — the fastest way to diagnose repeater/modal path scoping problems.
  • License is MIT, so forking into app/Filament/Components/ and dropping the dependency is fully legitimate. Given the maintenance risk profile, that's often the right call once you've confirmed the feature is ~100 lines — though v2.1.1 is a small enough surface that subclassing and staying on the dependency is now the lower-effort default.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin