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

Stimulus Controller Bundle Laravel Package

amorebietakoudala/stimulus-controller-bundle

A Laravel package that bundles reusable Stimulus controllers and helpers for adding interactive frontend behavior to Blade views with minimal JavaScript setup. Register controllers once and reuse them across your app for cleaner, lighter UI code.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. There is no service provider, no Laravel publish tag, and the PHP layer depends on webpack-encore-bundle — Laravel's default Vite pipeline will not consume it. Do not run composer require. What you want is the JavaScript.

Where to look first. Clone the repo and open assets/controllers/ (or src/Resources/assets/controllers/ depending on the tag). Read, in this order:

  1. table_controller.js — confirm-before-delete rows, the highest-value pattern.
  2. entity_controller.js — delete with return-URL handling.
  3. action_changer_controller.js — swap a form's action from any button.
  4. collection_controller.js — add/remove rows (Symfony-biased, expect to rewrite).

Skip the Select2 and datetimepicker controllers unless those libraries are already in your design system.

Minimal setup for a Laravel app.

npm install @hotwired/stimulus

Copy the one or two controllers you actually need into resources/js/controllers/:

resources/js/controllers/
  table_controller.js
  action_changer_controller.js

Register them in resources/js/app.js:

import { Application } from '@hotwired/stimulus'
import TableController from './controllers/table_controller'
import ActionChangerController from './controllers/action_changer_controller'

const app = Application.start()
app.register('table', TableController)
app.register('action-changer', ActionChangerController)

First real use case — confirm-before-delete on an admin index page:

<table data-controller="table">
  @foreach ($posts as $post)
    <tr>
      <td>{{ $post->title }}</td>
      <td>
        <a href="{{ route('posts.destroy', $post) }}"
           data-turbo-method="delete"
           data-action="click->table#confirm"
           data-table-confirmation="Delete this post?">
          Delete
        </a>
      </td>
    </tr>
  @endforeach
</table>

That is the whole day-one win: you get the pattern without inheriting the bundle's Symfony integration, its PHP 7.2.5 floor, or its dependency churn.

Implementation Patterns

Vendor, don't depend. The sustainable pattern is to treat the repo as reference source under MIT. Copy the controller body, strip Symfony-specific assumptions, and commit it to resources/js/controllers. You get grep-ability, source-map debugging, and no supply-chain tracking.

Controller contract via data attributes. All of these controllers speak only DOM:

<button data-action="click->action-changer#change"
        data-action-changer-url-value="{{ route('orders.store') }}"
        data-action-changer-method-value="POST">
  Save as new
</button>

Keep the data-*-value naming Stimulus convention (data-<identifier>-<name>-value) and rely on Stimulus's automatic type coercion for numbers, booleans, and arrays instead of hand-parsing.

Table confirmation flow. The table controller intercepts the click, shows the confirmation, and only fires the navigation/action when the user accepts (behaviour added in 1.0.8). Mirror that in your copy — a click handler that navigates before the confirm resolves is the most common bug when reimplementing:

async confirm(event) {
  const message = event.currentTarget.dataset.tableConfirmation || 'Are you sure?'
  if (!window.confirm(message)) {
    event.preventDefault()
    return
  }
  // only now allow navigation
}

JSON options override. The bundle exposes a data-options escape hatch (formalised in 1.0.9) so per-element behaviour can be overridden without forking:

<tr data-controller="table"
    data-options='{"confirmation": false, "template": "row"}'>

Keep this contract in your vendored version — it is genuinely useful for a "no confirm on this particular bulk toolbar" case.

Collection add/remove — reimplement, don't copy. The upstream collection_controller assumes Symfony's CollectionType prototype HTML and an incrementing index scheme. Laravel's equivalent is either a Blade @foreach with an index counter, or data-prototype markup you control:

<div data-controller="collection" data-collection-prototype-value="{{ $prototypeHtml }}">
  <div data-collection-target="items">
    @foreach ($rows as $i => $row)
      @include('admin.rows._row', ['index' => $i, 'row' => $row])
    @endforeach
  </div>
  <button data-action="click->collection#add">Add row</button>
</div>

Your controller needs add() to substitute __INDEX__ placeholders and remove() to splice the target list. That is ~40 lines of code and will be your own forever.

Action-changer integration. This is the cheapest lift after table. Wrap the form, put the button inside it, and set formaction-equivalent behaviour:

<form method="POST" action="{{ route('posts.update', $post) }}"
      data-controller="action-changer">
  @csrf @method('PUT')
  <button data-action="click->action-changer#change"
          data-action-changer-url-value="{{ route('posts.destroy', $post) }}"
          data-action-changer-method-value="DELETE">
    Delete instead
  </button>
</form>

The controller writes into a hidden _method input on submit. Validate that the button's type="submit" triggers the same path — the upstream controller listens on formdata (via a submit or formdata event), which is the correct hook for Turbo.

Turbo / Livewire compatibility. Both controllers must survive a Turbo Frame re-render. Use Stimulus's connect() / disconnect() lifecycle rather than assuming DOMContentLoaded. If you are mixing this with Livewire, prefer Stimulus for ephemeral UI and Livewire for server round-trips — do not let both try to own the same DOM node.

Blade partials keep markup clean. Wrap the row markup in a partial so the collection controller's prototype and the server-rendered row use the same template. This is the difference between a maintainable copy and a copy you grow to hate.

Gotchas and Tips

Composer will fail or drag in dead weight. webpack-encore-bundle ^1.11 || ^2.1 is a hard runtime requirement in composer.json. Installing on Laravel pulls Symfony's Encore bridge, a PHP 7.2.5+ floor, and a bundle registration path that is inert in a Laravel app. If you are determined to install it, isolate it in a separate package/service, not your main app.

The Symfony Collection controller is not reusable as-is. Even if you copy it, the DOM it expects (<li> with data-prototype, Symfony's index placeholders) is different from Laravel's. Budget a rewrite, not a copy, for this one.

Autofocus default changed in 1.0.12. Select2 dropdowns now autofocus by default. This is user-facing behaviour; if your accessibility review rejects forced focus movement, override via data-options or remove the line — do not silently absorb it.

Option-override semantics changed in 1.0.9. Controllers in versions prior to 1.0.9 ignore parts of data-options. If you copy an older tag's source and pair it with newer markup, you will ship silently wrong options. Pin your copy to a tag and note the version in a comment.

Return-URL bugs fixed in 1.0.7. If you copy an older tag's entity or table controller and your return URL has query parameters (e.g. an index page with filter state), the redirect will drop them. Copy from 1.0.7 or later, or port the fix yourself:

const url = new URL(returnUrl, window.location.origin)
url.searchParams.set('deleted', '1')
window.location.assign(url.toString())

Datetime picker churn. Versions 1.0.1, 1.0.5, and 1.0.15 all exist to patch or pin the datetime picker dependency. If you do not copy that controller, skip the library entirely. If you do, pin the picker version explicitly in your package.json and own the upgrade.

No tests, no CI. Nothing in the bundle exercises the controllers. When you vendor them, add a thin Playwright or capybara-style test for the two behaviours that matter (confirm-before-navigate, action-swap-on-submit). Those two failing silently in production is the realistic failure mode.

The listed release date is in the future. Confirm the actual latest tag before quoting "maturity" in any review. Treat the package as "small, unmaintained reference code" regardless of what the date says.

Stimulus devtools. Install the official Stimulus devtools extension before wiring up the first controller. Nine out of ten "nothing happens" reports are a missing data-controller attribute or a mismatched controller name (kebab-case in markup, PascalCase in registration). console.log(document.querySelector('[data-controller]')) and app.controllers will resolve most of them in under a minute.

Override, don't subclass. Because the controllers are small and communicate through data-*, the intended extension point is data-options values, not JavaScript inheritance. Adding a subclass to a vendored controller makes the override invisible to the next developer. If you need new behaviour, edit the copy and add a comment explaining why — you own it now.

Bus factor 1, MIT license, 0 dependents. Do not rely on upstream for fixes. Fork into resources/js/controllers, credit the source in a header comment, and treat any future upstream change as an idea to evaluate, not an upgrade to apply.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource