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

Js Twig Bundle Laravel Package

survos/js-twig-bundle

A Symfony bundle from Survos that streamlines integrating JavaScript into Twig templates, with helpers and configuration for managing scripts, assets, and dynamic front-end behavior alongside your Twig views.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations. survos/js-twig-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/js_twig.php, no Blade integration, and no Vite story — it targets Symfony's AssetMapper (importmaps, no build step) plus symfony/stimulus-bundle and symfony/ux-icons. If your Laravel app is your only PHP surface, you will not composer require this and get value; you will instead either (a) borrow the pattern, or (b) use it in a Symfony-based admin/backoffice that sits next to your Laravel app.

The value proposition is genuinely useful and has a clean Laravel analogue: one template definition, rendered server-side and client-side, so you stop hand-writing the same markup twice in Blade/Twig and in JS template literals.

Mental map for a Laravel dev:

Symfony helper Laravel equivalent
path() in JS Ziggy's route()
stimulus_controller() / stimulus_action() none first-party
ux_icon() blade-ui-kit/blade-icons
AssetMapper + importmap Vite + @vite

Where to look first (in order):

  1. The bundle's Twig extension — it is the list of helpers that must exist on both sides.
  2. The JS entry point for twig-browser — that is where path(), stimulus_*() and ux_icon() get registered as Twig.js functions.
  3. The route-as-JSON asset source — the thing AssetMapper serves so path() works in the browser.
  4. One shipped example template with a {% block %} — that is the unit of reuse.

Minimal wiring (Symfony side, roughly):

composer require survos/js-twig-bundle
php bin/console importmap:install
php bin/console importmap:require twig-browser

Register the bundle in config/bundles.php, then confirm the route JSON is reachable from the browser (view source / network tab) before writing any rendering code. That single check tells you whether AssetMapper is wired correctly or not.

First use case to ship: a table row or card that the server renders initially, and that the client re-renders after a fetch — same block, same context shape. Not a whole page. Fragments only.

{# templates/_row.html.twig #}
{% block row %}
  <tr data-id="{{ item.id }}">
    <td>{{ item.name|e }}</td>
    <td>{{ ux_icon('bi:pencil') }}</td>
    <td>
      <button {{ stimulus_action('row', 'edit', 'click') }}>Edit</button>
      <a href="{{ path('item_show', {id: item.id}) }}">View</a>
    </td>
  </tr>
{% endblock %}

Implementation Patterns

1. Block-as-fragment (the core pattern). Load the template source into twig-browser, render a named block with a context object, and swap the DOM node. The template string is the contract; the context is the data contract. Keep both stable and everything else is plumbing.

import { controller } from '@hotwired/stimulus';
// import shape depends on the twig-browser entry the bundle maps; check importmap.php

export default class extends controller {
  static targets = ['body'];

  async refresh(item) {
    const res = await fetch('/api/items/' + item.id);
    const ctx = await res.json();
    this.bodyTarget.innerHTML = this.twig.render('_row.html.twig', 'row', { item: ctx });
  }
}

2. One context normalizer, owned by PHP. The biggest source of bugs is PHP-side data (entities, DateTime, enums) reaching JS in a different shape than the Twig server render expected. Define a single serializer that emits plain scalars/ISO strings, and use it for both the full-page render and the JSON endpoint. If the server render gets a DateTime object and the client render gets a string, your |date() filter will quietly diverge.

3. Routes as data, never hardcoded. path() in the browser should mirror the Symfony router exactly. Fetch it once, cache it in a module-level singleton, and let a missing route name throw loudly rather than emitting /undefined. This is the same discipline Ziggy forces on Laravel teams — treat route generation as a build artifact, not a string you type.

4. stimulus_* helpers are what make client-rendered markup actually work. Hand-writing data-controller="row" in a JS template literal is where teams drift. Render the attributes through the helpers so freshly-inserted DOM carries valid data-action/data-*-target attributes. Stimulus picks up inserted nodes via its MutationObserver, so newly rendered fragments are connected automatically — but only if the attributes are correct and the controller identifier is registered.

5. Server-first, client-second. Always render the fragment on the server on first paint; use twig-browser only for subsequent updates. This keeps SEO, no-JS behaviour, and time-to-content intact, and it gives you a free fallback if Twig.js throws.

6. Laravel-side pattern extraction (if you're not on Symfony). The transferable ideas are: (a) an endpoint that returns structured context rather than HTML, plus (b) a client-side templating step. The Laravel-native way to get 80% of this is a JSON route manifest (Ziggy) plus a single re-render endpoint returning context for an Alpine/Blade::render-style partial, or simply returning view('partials.row', $ctx)->render() and letting the server do the rendering. That last option eliminates the whole dual-runtime problem — seriously consider it before porting Twig.js into a Laravel app.

7. Progressive rollout. Migrate one interactive list at a time. Each migration is: extract block → add JSON endpoint → add Stimulus controller → delete the JS string-template version. Never leave both in place.

Gotchas and Tips

Twig.js is not Twig. Missing filters/functions, no server-side extension support, different attribute() semantics, different macro and whitespace handling, and autoescaping that you must verify rather than assume. Keep the shared templates tiny and dumb: no include, no embed, no custom filters, no inheritance chains. If a template needs a Twig feature Twig.js doesn't have, that template is not a shared template.

Escaping is your XSS boundary. Two distinct injection surfaces: the context (safe — Twig escapes it) and the template source (catastrophic — never interpolate user data into template strings). Any "dynamic template from the database" feature is an XSS CVE waiting to happen.

AssetMapper has a dev/prod split. In dev, assets are served dynamically; in prod you must run asset-map:compile. Teams routinely ship a working dev environment and a broken prod one because the route JSON or twig-browser entry was never compiled. Add a smoke test against the compiled output.

Template source changes need cache-busting. If you fetch template sources over HTTP, a redeploy that changes a block will keep serving the stale version. Version the template URL or include the asset digest.

The release notes don't match the package. 1.6.0 is documented only as "Hack (not via survos) to remove dumps" — a hygiene patch, not a feature. That has two consequences: (a) you cannot infer the API from the changelog and must read the source, and (b) it is a maintenance-risk signal. Pin the version exactly, review the diff before upgrading, and add a CI gate (grep -rn "dump(\|dd(\|var_dump(" src/ templates/) so a future re-introduction of debug calls is caught rather than shipped. Zero stars, single-maintainer, and release notes describing a "hack" outside the upstream project are all reasons to treat this as a dev-only, non-critical dependency with a fallback plan.

Debugging Twig.js failures is painful. Errors surface as opaque render-time exceptions. Wrap the render call, and on failure log the template name, the block name, and the full context object — without that, you will be bisecting JSON payloads by hand.

Extension points worth knowing: custom JS functions/filters registered on the twig-browser instance, a custom route provider (if you need extra routes or non-Symfony URLs in path()), and the Stimulus controller that orchestrates hydration. Prefer registering a new JS function over forking the bundle.

Don't move the page to the client. The temptation after the first successful fragment is to render a whole view in the browser "since Twig works both places." That trades a working server-rendered page for hydration bugs, flash-of-empty-content, and a second templating runtime to debug. Fragments only.

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.
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
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource