stinger/bootstrap-maxlength-js-bundle
A Laravel bundle that packages the bootstrap-maxlength plugin with ready-to-use assets, so you can add live character counters and input length limits to Bootstrap forms without pulling in npm or managing vendor files manually.
This bundle is a thin Symfony2 asset wrapper around the bootstrap-maxlength jQuery plugin (the character counter that renders next to Bootstrap inputs). It ships no PHP logic beyond bundle registration — treat it as "vendor the JS via Composer" rather than as a framework feature.
Minimal steps
composer require stinger/bootstrap-maxlength-js-bundle
app/AppKernel.php. The class lives at the package root; conventionally:
public function registerBundles()
{
$bundles = array(
// ...
new Stinger\BootstrapMaxLengthBundle\StingerBootstrapMaxLengthBundle(),
);
}
Verify the exact class name first — this package has no docs worth trusting:
ls vendor/stinger/bootstrap-maxlength-js-bundle/
cat vendor/stinger/bootstrap-maxlength-js-bundle/*Bundle.php
web/:
php app/console assets:install web --symlink
<script src="{{ asset('bundles/stingerbootstrapmaxlength/js/bootstrap-maxlength.min.js') }}"></script>
Confirm the real path with find vendor/stinger/bootstrap-maxlength-js-bundle/Resources -name '*.js' rather than copying the line above blindly.{{ form_row(form.message, {'attr': {'maxlength': 140, 'data-maxlength-counter': 'true'}}) }}
$(function () {
$('[data-maxlength-counter]').maxlength({
alwaysShow: true,
threshold: 20,
warningClass: 'label label-success',
limitReachedClass: 'label label-warning'
});
});
Where to look first
Resources/public/js/ — the vendored plugin, and its version (check the banner comment; it dates the JS, not the bundle).load()/no extension, that confirms there is nothing else to learn.composer.json of the package — whether it declares symfony/framework-bundle constraints at all.Global, delegated initialization (the default pattern). One script in your base layout, initialized from data attributes rather than hardcoded selectors. This survives AJAX, dynamically added collection rows, and per-form variations:
$(document).on('focus', '[maxlength][data-counter]', function () {
var $el = $(this);
if ($el.data('maxlength-initialized')) { return; }
$el.data('maxlength-initialized', true).maxlength({
alwaysShow: $el.data('counter-always') === true,
threshold: $el.data('counter-threshold') || 10,
warningClass: 'text-warning',
limitReachedClass: 'text-danger'
});
});
Note the focus trigger rather than ready — the plugin needs the element to be laid out to measure placement correctly, and in modals/hidden tabs it will mis-position if initialized while display:none.
Symfony form integration. Keep the limit in one place — the form type — and let the attribute drive both HTML5 validation and the counter:
$builder->add('message', TextareaType::class, [
'attr' => [
'maxlength' => 140,
'data-counter' => true,
],
'constraints' => [new Assert\Length(['max' => 140])],
]);
A small form theme (form_theme.html.twig or form_div_layout.html.twig override) that auto-adds data-counter to any widget with maxlength is the highest-leverage move — it removes the per-field attr noise across an entire app:
{% block form_widget_simple %}
{% set attr = attr|merge({'data-counter': 'true'}) %}
{{ parent() }}
{% endblock %}
Pair with Assetic / javascripts block if the project already uses it, so the plugin gets minified and versioned with the rest of your JS instead of a bare <script> tag. Otherwise just asset() it — Assetic is overkill for one file.
AJAX and collection forms. Dynamic rows are the main real-world need. Initialize on collection:entry:added / your ajaxComplete handler, or use the delegated focus pattern above and skip the bookkeeping entirely.
Server/client parity. The maxlength attribute is advisory; the Length constraint is the contract. Keep them generated from the same constant or form option so a limit change can't drift.
.label → .badge), or Symfony 4+ bundle registration (config/bundles.php). Budget for forking or replacing it with a plain npm/Webpack Encore dependency — for a single jQuery file, the bundle buys you almost nothing over cp node_modules/bootstrap-maxlength/src/bootstrap-maxlength.js assets/js/.assets:install is not optional. A 404 on the script is the #1 "bundle doesn't work" symptom. On newer Symfony without assets:install, symlink manually into public/bundles/.typeof $.fn.maxlength must be 'function'. If it's undefined, it's an include-order or path problem, not a config problem.maxlength vs. the counter are subtly at odds. Native maxlength truncates input, so the "limit reached" red state only fires at exactly the limit, never beyond — that's expected, not a bug. Don't strip the attribute to get the red state; you'd lose the no-JS fallback and native validation. If you want soft limits, drop maxlength and enforce Length server-side only, but then the plugin has no limit to read and you must set the attribute before init.utf8: true counts code points, while PHP's Assert\Length counts via mb_strlen. They mostly agree, but surrogate pairs and combining characters will produce off-by-N mismatches between the badge and your validation error. Test with emoji if your users type them.position: relative parents. Wrong placement is almost always a container issue. Try appendToParent: true, or pass an explicit placement: 'top'/'bottom', and re-initialize after any animation/modals finished opening.shown.bs.tab or lazily on first focus..maxlength() twice on the same element appends a second badge. The data-*-initialized guard above is the cheap fix; .maxlength('destroy') style teardown is unreliable in the vendored version — verify before relying on it.maxlength-init.js) in your app bundle that reads data-* attributes — not a PR to a 2015 bundle nobody maintains.composer.json and commit composer.lock. With no releases since 2015 there is no security maintenance; you are accepting the vendored JS as-is.AppKernel and asset publishing that no longer exists in that shape. Copy the JS into assets/ and add it to your Encore entry — same result, no dead abstraction.How can I help you explore Laravel packages today?