Download the PHP package mediagone/vue-in-twig-bundle without Composer
On this page you can find all versions of the php package mediagone/vue-in-twig-bundle. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download mediagone/vue-in-twig-bundle
More information about mediagone/vue-in-twig-bundle
Files in mediagone/vue-in-twig-bundle
Package vue-in-twig-bundle
Short Description Integrates Vue.js 3 into Symfony/Twig without any Node.js/npm ecosystem — extends it with Twig's server-side power to compose your components, generate safe Symfony URLs with dynamic parameters, and inject PHP constants or initial data directly into them.
License MIT
Informations about the package vue-in-twig-bundle
mediagone/vue-in-twig-bundle
Integrates Vue.js 3 into Twig/Symfony templates and extends Vue's capabilities with Twig's server-side power: slots, extends, embed...
Compose your components, inject PHP constants or initial data directly into them and generate safe Symfony URLs with dynamic parameters.
No Node.js toolchain required: no bundler, no build step, no node_modules...
The primary objective is to automate and simplify using Vue inside a Symfony/Twig app, without adding a JS build toolchain on top of it.
Table of contents
- Installation
- Configuration
- Introduction
- Credo: PHP as the single source of truth
- What Twig brings that Vue alone cannot do
- Get started
- Create a Vue application
- Declare and include components
- Override the default configuration
- Differences from standard Vue.js
- Delimiters
- Injecting server-side data into Vue props
- Injecting PHP constants into Vue expressions
- Generate safe URLs for Symfony's routes
- File naming convention
- Twig composition over Vue slots
- Examples
- Local development
Installation
This package requires PHP 8.1+, Twig 3 and "symfony/framework-bundle" ^6.1|^7.0
-
Add it as Composer dependency:
-
Register the bundle in
config/bundles.php: -
Load Vue 3 full build (with compiler) in your layout. \ Note: the compiler build is required since there is no precompile step, the x-templates are compiled in the browser at runtime.
- A few components also call API endpoints via axios — load it once if you use any of these:
ToggleButton,UploadZone,DataList,DataEditor
Introduction
This bundle formalizes a specific integration pattern: Vue components are written as x-templates, rendered and composed server-side by Twig.
PHP as the single source of truth
Beyond simplifying the front-end toolchain, the core benefit of rendering Vue server-side with Twig is that PHP stays the single source of truth, automatically kept in sync with the front-end.
Server-side values — enum cases, constants, config, URLs — flow into the Vue UI at render time, so there is no hand-maintained JS duplicate that silently drifts out of sync when the PHP changes.
A concrete example: a <select> populated by iterating a PHP enum's cases in Twig.
Add, rename or remove a case in BlockType, and the dropdown updates on the next render — no parallel JS array to keep in sync. \
The same idea applies to v-if checks against a status, a list of allowed types, a feature flag, etc. (see Injecting PHP constants into Vue expressions below).
What Twig brings that Vue alone cannot do:
- Compose components server-side (Twig blocks/embeds — see Twig composition over Vue slots)
- Inject PHP constants without an API call:
v-if="type === '{{ constant('Domain\\Block::TYPE_A') }}'" - Inject initial data without an API call:
:account="{{ account|vue_json_encode }}" - Generate type-safe Symfony URLs with dynamic Vue expressions, via
vue_path()
Get started
Everything is wired from your layout via Twig tags and functions — there is no .js entry file to write by hand.
Use {% vue_app %} to create and mount automatically your Vue application:
Declare required components to be queued for inclusion with the {% vue_use %} tag:
Every {% vue_use %} tag must be used within the {% vue_app %} tags — whether placed in the same template or in any included or extended template:
Example:
Layout.twig:
Page.twig:
Partial.twig:
Placed in your base layout, vue_app will output:
- Opening tag →
<script>window.VUE_APP = Vue.createApp(window.VUE_ROOT ?? {});</script>+setup.js(delimiters, global mixin) - Body → rendered normally;
{% vue_use %}tags queue components silently (no output) - Closing tag → all queued component templates + scripts (deduplicated, in call order) +
<script>VUE_APP.mount('selector');</script>
Differences from standard Vue.js
Delimiters
Vue's default {{ }} delimiters conflict with Twig, so Vue-in-twig reconfigures them to [[ ]]. \
Use [[ ]] everywhere Vue reactivity is needed — in x-templates (.vue.twig) and in the mounted HTML.
The two template engines coexist in the same markup, each running at a different time:
| Engine | Runs | Syntax | |
|---|---|---|---|
| Server | Twig | At request time (PHP) | {{ }} |
| Client | Vue | In the browser (JS) | [[ ]] |
Twig composition over Vue slots
Components in this bundle are extended/composed with Twig ({% embed %} + blocks — server-side composition) rather than Vue's slots, because they are too limited for and offer less customization. This is why a complex component like DataList exposes Twig blocks instead of Vue slots for its markup — see the DataList example.
Simple, purely visual customization points (a button label, a small fragment of markup) still use regular Vue slots — e.g. Modal's header/footer, DropZone's instructions/infos...
Injecting server-side data into Vue props
Twig {{ }} still works inside HTML attributes — Twig renders the attribute value as a string, and Vue reads it as a JS expression:
Both lines above write the value raw, with no JSON encoding — safe here given the nature of these values: app.request.locale and maxFileSizeBytes can't contain characters that would break the expression. For anything else (user input, free text, structured data...) this library provides the |vue_json_encode filter, which is an HTML-safe replacement for |json_encode that serializes and escapes the value safely:
_Note: vue_json_encode applies JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_THROW_ON_ERROR._
Injecting PHP constants into Vue expressions
Constants and enums can be injected directly into Vue attribute values — Twig renders them as literal strings before Vue compiles the template.
Generate safe URLs for Symfony's routes
Symfony URLs combining static and dynamic, Vue-side parameters can be safely generated via the vue_path(route, staticParams, dynamicParams) function (similar to Symfony's path()):
Generates: '/ajax/account?accountId=__ACCOUNTID__'.replace('__ACCOUNTID__', account.id)
File naming convention
.vue.twig + .vue.js — immediately identifies Vue files among other Twig templates.
Placed in your base layout, it'll output:
- Opening tag →
<script>window.VUE_APP = Vue.createApp(window.VUE_ROOT ?? {});</script>+setup.js(delimiters, global mixin) - Body → rendered normally;
{% vue_use %}tags queue components silently (zero output) - Closing tag → all queued component templates + scripts (deduplicated, in call order) +
<script>VUE_APP.mount('selector');</script>
VUE_ROOT — root component options
Vue.createApp() is called with window.VUE_ROOT ?? {}. Declare root-level data()/methods/etc. globally before {% vue_app %} runs:
Tip — mount placement
Vue replaces the mount target's content (container.innerHTML = '') before mounting. Since {% endvue_app %} outputs the queued x-templates and component scripts, place {% vue_app %}...{% endvue_app %} after the element you mount onto (e.g. after </div> closing #App), not inside it — otherwise the x-templates get wiped out before Vue can read them.
Declare and include components (2)
Vue components are declared and queued for inclusion via the {% vue_use %} tag:
Can be called from any partial, before {% endvue_app %}. Duplicate calls are ignored (include-once); the queue otherwise preserves call order. Each call queues two files (if they exist):
Category/ComponentName.vue.twig— the x-templateCategory/ComponentName.vue.js— the component registration
By default, a bare 'Category/Name' resolves against the bundle's own @VueInTwig namespace — this is what you use for every built-in component shown in Examples.
Registering your own components
By default, a bare 'Category/Name' resolves against the bundle's own @VueInTwig namespace — this is what you use for every built-in component shown in examples.
{% vue_use %} also accepts an explicit @Namespace/... reference, used as-is instead of being prefixed. This lets a consuming app register its own Vue components through the same queue/dedup mechanism — typically to extend a bundle component (see the DataList example).
Without this config, the default namespace stays @VueInTwig, so every bare {% vue_use 'Category/Name' %} keeps resolving to the bundle's own components — no change for the common case.
Order matters. The queue is flushed in call order, immediately before VUE_APP.mount(). A component that extends another (e.g. VUE_APP.component('vue-datalist')) must be {% vue_use %}'d after its base — otherwise the base isn't registered yet when the extending component reads it.
Override the default configuration
The default configuration can be overridden via the vue_config function or tag, which populates window.VUE_CONFIG — read by setup.js and by components such as DataList. Two complementary forms:
The dot-path maps to VUE_CONFIG.root.key ('search.debounceMs' → VUE_CONFIG.search = { debounceMs: 500 };). Both forms write the same way and can target the same root key from different places; the buffered config is flushed once as a single <script> block at {% endvue_app %}, replacing the old pattern of an inline <script>VUE_CONFIG.x = ...;</script> override placed by hand in the body.
setup.js mixins and helpers
setup.js is rendered automatically by the opening {% vue_app %} tag. It provides:
VUE_APP.config.compilerOptions.delimiters |
Set to ['[[', ']]'] |
format_date(value, locale, options) |
Global mixin method — Intl.DateTimeFormat wrapper |
month_name(month) |
Global mixin method — localized month name for a 1-12 month number |
slugify(str) |
Global mixin method — ASCII slug |
window.debounce(fn, ms) |
Helper used internally by DataList; overridable via ??= |
window.VUE_CONFIG |
Defaults to { debounceSearch: 300 }, overridable via vue_config |
Examples
Each example assumes the component was declared with {% vue_use %} and Vue 3 (+ axios where noted) is loaded, as described in Get started. Props tables list every prop declared on the component; "Required" props have no default.
Controls
DatePicker (vue-date-picker)
Date selection input (year / month / day selects).
Props
| Prop | Type | Default | Required |
|---|---|---|---|
initialDate |
Date |
— | ✓ |
yearsBefore |
Number |
2 |
|
yearsAfter |
Number |
3 |
|
yearsList |
Array |
null (computed from yearsBefore/yearsAfter) |
Emits: dateSelected (the new Date)
DatetimePicker (vue-datetime-picker)
Date + time selection input.
Props
| Prop | Type | Default | Required |
|---|---|---|---|
initialDate |
Date |
— | ✓ |
useTime |
Boolean |
true |
|
showAllMinutes |
Boolean |
false (otherwise rounded to 5-minute steps) |
|
yearsBefore |
Number |
2 |
|
yearsAfter |
Number |
3 |
|
yearsList |
Array |
null |
|
futureDateText |
String |
'' |
|
pastDateText |
String |
'' |
Emits: dateSelected (the new Date)
DropZone (vue-drop-zone)
File selection + validation + a confirmation preview modal. Does not upload — it only emits the selected files; the parent handles the actual upload (see UploadZone for an integrated alternative).
Props
| Prop | Type | Default | Required |
|---|---|---|---|
title |
String |
— | ✓ |
selectionLimit |
Number |
0 (unlimited) |
|
fileMaxSize |
Number |
0 (unlimited), in bytes |
|
fileMimeTypes |
String |
'' (any), comma-separated |
Emits: select (array of valid File objects, once confirmed in the preview modal)
Slots
| Slot | Scope | Description |
|---|---|---|
instructions |
fileInput |
Replaces the default "drag & drop or browse" text |
infos |
formats, maxSize |
Replaces the default formats/size hint |
UploadZone (vue-upload-zone)
File selection with an integrated upload (axios) and an optional built-in crop step (embeds ImageCropper) before sending.
Props
| Prop | Type | Default | Required |
|---|---|---|---|
postUrl |
String |
— | ✓ |
postParameterName |
String |
— | ✓ |
title |
String |
— | ✓ |
dropText |
String |
— | ✓ |
allowedFileTypes |
String |
'', comma-separated |
|
allowMultipleFiles |
Boolean |
false |
|
maxFileSize |
Number |
0 (unlimited), in bytes |
|
dropInfoText |
String |
'({formats} <= {maxSize})' — placeholders: {formats}, {outputWidth}, {outputHeight}, {maxSize} |
|
editorEnabled |
Boolean |
false — crop step for a single image (png/jpeg) before upload |
|
editorOutputWidth / editorOutputHeight |
Number |
0 (natural crop size) |
|
editorOutputFormat |
String |
'' (same as source) |
|
editorFixedRatio |
Boolean |
false |
|
sendButtonLabel / cancelButtonLabel / okButtonLabel |
String |
'Envoyer' / 'Annuler' / 'OK' |
|
fileTooLargeTitle / fileTooLargeText |
String |
text supports {filename}, {size}, {maxsize} |
|
uploadFailureTitle / uploadFailureText |
String |
shown if the axios POST fails |
Emits: uploaded (the server response's results)
ImageCropper (vue-image-cropper)
Interactive crop UI (8 resize handles + move) over a source image, rendering the crop to a <canvas>.
Props
| Prop | Type | Default | Required |
|---|---|---|---|
sourceDataUrl |
String |
— | ✓ |
outputWidth / outputHeight |
Number |
0 (natural crop size) |
|
outputMimeFormat |
String |
'' (same as source) |
|
fixedRatio |
Boolean |
false (hold Shift while dragging to force it ad hoc) |
Emits: cropped (a Blob, from canvas.toBlob())
SwitchButton (vue-switch-button)
Toggle switch that is parent-controlled: it never mutates the bound object itself, it only asks for the change.
Props
| Prop | Type | Default | Required |
|---|---|---|---|
object |
Object |
— | ✓ |
property |
String |
— | ✓ |
valueOn |
String\|Boolean\|Number |
— | ✓ |
valueOff |
String\|Boolean\|Number |
— | ✓ |
disabled |
Boolean |
false |
Emits: switch-request (the would-be next value — the parent decides whether/how to apply it, e.g. after an API call)
ToggleButton (vue-togglebutton)
Two-state button that is API-driven, unlike SwitchButton: it fetches its own current value on creation and posts the change itself.
Props
| Prop | Type | Default | Required |
|---|---|---|---|
api_url |
String |
— | ✓ |
result_name |
String |
— | ✓ |
result_property |
String |
— | ✓ |
value_on / value_off |
String |
'1' / '0' |
|
confirm_on / confirm_off |
String |
'' (no confirmation) |
|
disabled |
Boolean |
false |
On creation, performs GET {api_url}?fields={result_property} and reads response.data.results[result_name][result_property]. On click, POSTs the new value the same way and updates from the response. No emits — state lives in the component.
Layout
Modal (vue-modal)
Props
| Prop | Type | Default | Required |
|---|---|---|---|
titleText |
String |
'' |
|
titleStyle |
String |
'' (e.g. 'warning', 'danger') |
|
yesButtonText / noButtonText |
String |
'' (hidden if empty) |
|
yesButtonClass / noButtonClass |
String |
'--primary' / '' |
|
yesButtonEnabled / noButtonEnabled |
Boolean |
true |
Emits: clickyes, clickno (from the default footer buttons)
Slots
| Slot | Description |
|---|---|
header |
Replaces the default title block |
| (default) | Modal body |
footer |
Replaces the default yes/no buttons (you then own the emits) |
LockWrapper (vue-lock-wrapper)
Locks/unlocks its content (e.g. a disabled form until the user explicitly unlocks it).
No props. Internal state: locked (defaults to true).
Slots
| Slot | Scope | Description |
|---|---|---|
content |
locked, lock, unlock, toggle |
The protected content |
button |
locked |
Label/icon of the lock toggle button (the <button> itself, already wired to toggle(), wraps this slot) |
Behaviors
Renderless components (no wrapper element) — they apply behavior directly to their single child.
AutoResize (vue-auto-resize)
Resizes its child (e.g. a <textarea>) to fit its content, on input and on window resize. No props, no emits — wraps exactly one child element.
Draggable (vue-draggable)
Native HTML5 drag & drop, zero dependency. Reorders a list's children and moves items between lists sharing the same group.
Props
| Prop | Type | Default | Required |
|---|---|---|---|
modelValue |
Array |
— | ✓ (use with v-model) |
group |
String |
null — two lists with the same non-null group accept moves between them |
|
sort |
Boolean |
true — reorder within this list |
|
emptyHeight |
String |
null — inline min-height forced while empty, so it stays droppable without CSS |
|
usePlaceholder |
Boolean |
false — gap placeholder instead of the default thin insertion line |
Emits: update:modelValue (new array), change (no payload) — the component mutates nothing in place, it re-emits new arrays.
Drop feedback is themable via CSS variables (on the list element or :root): --vue-draggable-indicator-color (#2684ff), --vue-draggable-indicator-size (2px), --vue-draggable-indicator-style (solid, line mode only), --vue-draggable-placeholder-bg.
Widgets
DataEditor (vue-data-editor)
Formalizes an inline-edit pattern: tracks whether item changed since it was loaded/saved, and shows a save bar only when there's something to save.
Props
| Prop | Type | Default | Required |
|---|---|---|---|
item |
Object |
— | ✓ |
postUrl |
String\|Function |
— | ✓ — a function receives item and must return the URL, for dynamic endpoints |
postUrlProperties |
String |
— | ✓ — comma-separated list of item keys to send |
resultPath |
String |
null |
dot-path into the response (e.g. 'results.portal') to sync item back from the server; omit to stay shape-agnostic |
upToDateText |
String |
'' |
shown when there's nothing to save; empty keeps that panel hidden |
No emits. Calling changed() (exposed in the default slot) re-checks whether item differs from its last-saved snapshot and shows/hides the save bar accordingly; save() posts the listed properties and re-snapshots on success.
Slots
| Slot | Scope | Description |
|---|---|---|
save-text |
— | Replaces the default "you have unsaved changes" text |
| (default) | item, originalItem, data ($data), props ($props), changed |
The editable form |
modal-error |
data ($data), props ($props) |
Replaces the default error message in the failure modal |
DataList (vue-datalist)
Formalizes a list pattern: fetch + pagination + create/delete, with debounced refresh for search/filter inputs. vue-datalist itself is logic only — it has no template and no slots. The markup comes from embedding Widgets/DataList.twig and overriding its Twig blocks; your own component then extends the base logic.
1. Base component — queue it like any other:
2. Your extending component — registered in your own app, under your configured namespace so it loads through the same queue, after the base:
3. The markup — embed the bundle's template, overriding only the blocks you need:
Props (on the base vue-datalist, inherited by your component)
| Prop | Type | Default | Required |
|---|---|---|---|
itemsListUrl |
String |
— | ✓ |
itemsCreateUrl |
String |
'' (create disabled) |
|
itemsDeleteUrl |
String |
'' (delete disabled), use a -ID- placeholder |
|
page |
Number |
1 |
|
config |
Object |
{} |
per-instance override, see below |
Twig blocks (Widgets/DataList.twig)
| Block | Default | Notes |
|---|---|---|
TOOLS_LEFT / TOOLS_RIGHT |
empty / refresh button | Toolbar content |
TABLE_HEADERS |
empty | <th> cells, inside the header <tr> |
TABLE_ROW |
empty | <td> cells for each row in items |
TABLE_BUSY |
loading text | Shown while isBusy |
TABLE_EMPTY |
"no results" text | Shown when items is empty |
BODY |
empty | Extra markup after the table (e.g. a "create" button) |
MODAL_CREATE / MODAL_DELETE |
empty | Body of the create/delete confirmation modals |
MODAL_ERROR |
error description | Body of the error modal |
Overridable hooks (override in your extending component's methods)
| Hook | Purpose |
|---|---|
parseResponse(response) |
Maps a successful list response to { items, page, pageCount, total }. Default reads response.data.payload; falls back to VUE_CONFIG.DataList.parseResponse if set |
parseErrorResponse(response) |
Extracts { code, description } from a failed response. Falls back to VUE_CONFIG.DataList.parseErrorResponse |
buildErrorModal(error, context) |
Builds the error modal object (title/description) from the extracted error; context is 'list'/'create'/'delete' |
rowKey(row) |
:key for each row — defaults to row.id ?? row |
rowAttributes(row) |
Extra attributes/listeners (e.g. onClick, class) merged onto each <tr> |
modifyUrlParameters(params) |
Push extra query params (e.g. search/filters) before each list request |
onItemsRefresh() / onItemsRefreshFailure() |
Called after a successful/failed refresh |
Call this.debounceRefresh() (instead of this.itemsRefresh()) from a search/filter input handler to debounce the request using VUE_CONFIG.debounceSearch.
config shape — { parseResponse, parseErrorResponse, icons, texts, tooltips }, merged over VUE_CONFIG.DataList (global default for every list, set via vue_config); icons/texts/tooltips merge per-key, so a partial override keeps the other defaults.