PHPackages                             mediagone/vue-in-twig-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. mediagone/vue-in-twig-bundle

ActiveSymfony-bundle[Templating &amp; Views](/categories/templating)

mediagone/vue-in-twig-bundle
============================

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.

0.3.1(3w ago)07MITJavaScriptPHP &gt;=8.1

Since Jun 9Pushed 3w agoCompare

[ Source](https://github.com/Mediagone/vue-in-twig-bundle)[ Packagist](https://packagist.org/packages/mediagone/vue-in-twig-bundle)[ RSS](/packages/mediagone-vue-in-twig-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (4)Versions (5)Used By (0)

mediagone/vue-in-twig-bundle
============================

[](#mediagonevue-in-twig-bundle)

[![Latest Stable Version](https://camo.githubusercontent.com/57c68a0e66f07eb335c88449abb61ebe22181e03c7a73926b5b96b3d84f8e241/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d65646961676f6e652f7675652d696e2d747769672d62756e646c65)](https://packagist.org/packages/mediagone/vue-in-twig-bundle)[![Total Downloads](https://camo.githubusercontent.com/83a39dd9548f00bd0aa9be0b97d649c1cc1148b398665aaa326b1a9b4d025978/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d65646961676f6e652f7675652d696e2d747769672d62756e646c65)](https://packagist.org/packages/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/npm ecosystem* required**: no bundler, no build step, no node\_modules...

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Configuration](#configuration)
- [Introduction](#introduction)
    - Credo: PHP as the single source of truth
    - What Twig brings that Vue alone cannot do
- [Get started](#get-started)
    - Create a Vue application
    - Declare and include components
    - Override the default configuration
- [Differences from standard Vue.js](#differences-from-standard-vuejs)
    - 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](#examples)
- [Local development](#local-development)

---

Installation
------------

[](#installation)

This package requires **PHP 8.1+**, **Twig 3** and **"symfony/framework-bundle" ^6.1|^7.0**

1. Add it as Composer dependency:

```
composer require mediagone/vue-in-twig-bundle
```

2. Register the bundle in `config/bundles.php`:

```
Mediagone\VueInTwigBundle\VueInTwigBundle::class => ['all' => true],
```

3. 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.*

```

```

4. A few components also call API endpoints via [axios](https://axios-http.com) — load it once if you use any of these: `ToggleButton`, `UploadZone`, `DataList`, `DataEditor`

```

```

Configuration
-------------

[](#configuration)

The Twig namespace `@VueInTwig/` is configured automatically (`VueInTwigBundle::prepend()`) — no `twig.yaml` changes needed.

---

Introduction
------------

[](#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*

[](#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 `` populated by iterating a PHP enum's cases in Twig.

```

    {% for case in App\Domain\BlockType::cases() %}
        {{ case.label }}
    {% endfor %}

```

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](#differences-from-standard-vuejs) below).

### What Twig brings that Vue alone cannot do:

[](#what-twig-brings-that-vue-alone-cannot-do)

- Compose components server-side (Twig blocks/embeds — see [Twig composition over Vue slots](#differences-from-standard-vuejs))
- 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
-----------

[](#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:

```
{% vue_app '#App' %}
  {% block CONTENT %}{% endblock %}
{% endvue_app %}
```

Declare required components to be queued for inclusion with the `{% vue_use %}` tag:

```
{% vue_app '#App' %}
  {% vue_use 'Controls/DatePicker' %}

  {% vue_use 'Layout/Modal' %}
  {% vue_use 'Layout/Modal' %}  {# ignored, if a component is declared twice, it'll only included once #}

  {% block CONTENT %}{% endblock %}
{% endvue_app %}
```

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:

[](#example)

*Layout.twig:*

```
{% vue_app '#App' %}
  {% vue_use 'Controls/DatePicker' %}
  {% vue_use 'Layout/Modal' %}
  ...
  {% block CONTENT %}{% endblock %}
{% endvue_app %}
```

*Page.twig:*

```
{% extends 'Layout.twig' %}

{% vue_use 'Controls/DatePicker' %} {# already included, ignored #}
{% vue_use 'Controls/SwitchButton' %}

{% block CONTENT %}

    {% include 'Partial.twig' %}

{% endblock %}
```

*Partial.twig:*

```
{% vue_use 'Controls/ToggleButton' %}

...
```

Placed in your base layout, `vue_app` will output:

1. **Opening tag** → `window.VUE_APP = Vue.createApp(window.VUE_ROOT ?? {});` + `setup.js` (delimiters, global mixin)
2. **Body** → rendered normally; `{% vue_use %}` tags queue components silently (no output)
3. **Closing tag** → all queued component templates + scripts (deduplicated, in call order) + `VUE_APP.mount('selector');`

---

Differences from standard Vue.js
--------------------------------

[](#differences-from-standard-vuejs)

### Delimiters

[](#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.

```
{# Vue reactive expression — evaluated in the browser #}
[[ item.title ]]
[[ count ]] items
```

The two template engines coexist in the same markup, each running at a different time:

EngineRunsSyntaxServerTwigAt request time (PHP)`{{ }}`ClientVueIn the browser (JS)`[[ ]]`### Twig composition over Vue slots

[](#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](#datalist-vue-datalist).

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

[](#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:

```
{# Static PHP value, no reactivity needed #}
:locale="'{{ app.request.locale }}'"
:max-size="{{ maxFileSizeBytes }}"
```

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:

```
{# Before — XSS risk #}
:account="{{ account|json_encode }}"

{# After — Prevents XSS when injecting PHP data as Vue props #}
:account="{{ account|vue_json_encode }}"
```

*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

[](#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.

```
v-if="block.type === '{{ constant('App\\Domain\\Block::TYPE_VIDEO') }}'"
:allowed-types="['{{ constant('App\\Domain\\Media::TYPE_IMAGE') }}', '{{ constant('App\\Domain\\Media::TYPE_PDF') }}']"
```

### Generate safe URLs for Symfony's routes

[](#generate-safe-urls-for-symfonys-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()`):

```
:url="{{ vue_path('ajax_account', {}, {accountId: 'account.id'}) }}"
```

Generates: `'/ajax/account?accountId=__ACCOUNTID__'.replace('__ACCOUNTID__', account.id)`

### File naming convention

[](#file-naming-convention)

`.vue.twig` + `.vue.js` — immediately identifies Vue files among other Twig templates.

```
{% vue_app '#App' %}
    {% block BODY_CONTENT %}{% endblock %}
{% endvue_app %}
```

Placed in your base layout, it'll output:

1. **Opening tag** → `window.VUE_APP = Vue.createApp(window.VUE_ROOT ?? {});` + `setup.js` (delimiters, global mixin)
2. **Body** → rendered normally; `{% vue_use %}` tags queue components silently (zero output)
3. **Closing tag** → all queued component templates + scripts (deduplicated, in call order) + `VUE_APP.mount('selector');`

#### `VUE_ROOT` — root component options

[](#vue_root--root-component-options)

`Vue.createApp()` is called with `window.VUE_ROOT ?? {}`. Declare root-level `data()`/`methods`/etc. globally **before** `{% vue_app %}` runs:

```

window.VUE_ROOT = {
    data() {
        return { showModal: false };
    },
};

{% vue_app '#App' %}
    ...
{% endvue_app %}
```

#### Tip — mount placement

[](#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 `` closing `#App`), not inside it — otherwise the x-templates get wiped out before Vue can read them.

### Declare and include components (2)

[](#declare-and-include-components-2)

Vue components are declared and queued for inclusion via the `{% vue_use %}` tag:

```
{% vue_use 'Controls/DatePicker' %}

{# Called twice → included once #}
{% vue_use 'Layout/Modal' %}
{% vue_use 'Layout/Modal' %}  {# ignored #}
```

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-template
- `Category/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](#examples).

#### Registering your own components

[](#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](#datalist-vue-datalist)).

```
# config/packages/vue_in_twig.yaml
vue_in_twig:
    default_namespace: '@App'   # default: '@VueInTwig'
```

```
# config/packages/twig.yaml
twig:
    paths:
        '%kernel.project_dir%/templates/vue': 'App'
```

```
{% vue_use '@VueInTwig/Widgets/DataList' %}  {# bundle component → explicit namespace #}
{% vue_use 'Portal/UsersList' %}              {# app component → resolved via default_namespace #}
```

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

[](#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:

```
{# Function — value must be JSON-serializable PHP #}
{{ vue_config('search.debounceMs', 500) }}

{# Tag — body is raw JS, for an already-JSON source or non-serializable values like functions #}
{% vue_config 'chart.options' %}
{ responsive: true, onClick: () => { /* ... */ } }
{% endvue_config %}
```

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 `` block at `{% endvue_app %}`, replacing the old pattern of an inline `VUE_CONFIG.x = ...;` override placed by hand in the body.

### setup.js mixins and helpers

[](#setupjs-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
--------

[](#examples)

Each example assumes the component was declared with `{% vue_use %}` and Vue 3 (+ axios where noted) is loaded, as described in [Get started](#get-started). Props tables list every prop declared on the component; "Required" props have no default.

### Controls

[](#controls)

#### DatePicker (`vue-date-picker`)

[](#datepicker-vue-date-picker)

Date selection input (year / month / day selects).

```
{% vue_use 'Controls/DatePicker' %}

```

**Props**

PropTypeDefaultRequired`initialDate``Date`—✓`yearsBefore``Number``2``yearsAfter``Number``3``yearsList``Array``null` (computed from `yearsBefore`/`yearsAfter`)**Emits:** `dateSelected` (the new `Date`)

#### DatetimePicker (`vue-datetime-picker`)

[](#datetimepicker-vue-datetime-picker)

Date + time selection input.

```
{% vue_use 'Controls/DatetimePicker' %}

```

**Props**

PropTypeDefaultRequired`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`)

[](#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).

```
{% vue_use 'Controls/DropZone' %}

```

**Props**

PropTypeDefaultRequired`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**

SlotScopeDescription`instructions``fileInput`Replaces the default "drag &amp; drop or browse" text`infos``formats`, `maxSize`Replaces the default formats/size hint#### UploadZone (`vue-upload-zone`)

[](#uploadzone-vue-upload-zone)

File selection with an **integrated upload** (axios) and an optional built-in crop step (embeds `ImageCropper`) before sending.

```
{% vue_use 'Controls/UploadZone' %}

```

**Props**

PropTypeDefaultRequired`postUrl``String`—✓`postParameterName``String`—✓`title``String`—✓`dropText``String`—✓`allowedFileTypes``String``''`, comma-separated`allowMultipleFiles``Boolean``false``maxFileSize``Number``0` (unlimited), in bytes`dropInfoText``String``'({formats}
```

**Props**

PropTypeDefaultRequired`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`)

[](#switchbutton-vue-switch-button)

Toggle switch that is **parent-controlled**: it never mutates the bound object itself, it only asks for the change.

```
{% vue_use 'Controls/SwitchButton' %}

```

**Props**

PropTypeDefaultRequired`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`)

[](#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.

```
{% vue_use 'Controls/ToggleButton' %}

```

**Props**

PropTypeDefaultRequired`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, `POST`s the new value the same way and updates from the response. No emits — state lives in the component.

### Layout

[](#layout)

#### Modal (`vue-modal`)

[](#modal-vue-modal)

```
{% vue_use 'Layout/Modal' %}

    Modal content.

```

**Props**

PropTypeDefaultRequired`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**

SlotDescription`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`)

[](#lockwrapper-vue-lock-wrapper)

Locks/unlocks its content (e.g. a disabled form until the user explicitly unlocks it).

```
{% vue_use 'Layout/LockWrapper' %}

        [[ locked ? 'Unlock' : 'Lock' ]]

```

No props. Internal state: `locked` (defaults to `true`).

**Slots**

SlotScopeDescription`content``locked`, `lock`, `unlock`, `toggle`The protected content`button``locked`Label/icon of the lock toggle button (the `` itself, already wired to `toggle()`, wraps this slot)### Behaviors

[](#behaviors)

Renderless components (no wrapper element) — they apply behavior directly to their single child.

#### AutoResize (`vue-auto-resize`)

[](#autoresize-vue-auto-resize)

```
{% vue_use 'Behaviors/AutoResize' %}

```

Resizes its child (e.g. a ``) to fit its content, on input and on window resize. No props, no emits — wraps exactly one child element.

#### Draggable (`vue-draggable`)

[](#draggable-vue-draggable)

Native HTML5 drag &amp; drop, zero dependency. Reorders a list's children and moves items between lists sharing the same `group`.

```
{% vue_use 'Behaviors/Draggable' %}

        [[ it.label ]]

```

**Props**

PropTypeDefaultRequired`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

[](#widgets)

#### DataEditor (`vue-data-editor`)

[](#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.

```
{% vue_use 'Widgets/DataEditor' %}

```

**Props**

PropTypeDefaultRequired`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 hiddenNo 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**

SlotScopeDescription`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`)

[](#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:

```
{% vue_use 'Widgets/DataList' %}
```

**2. Your extending component** — registered in your own app, under your configured [namespace](#configuration) so it loads through the same queue, *after* the base:

```
{% vue_use 'Portal/UsersList' %}
```

```
// templates/vue/Portal/UsersList.vue.js (or wherever your '@App' namespace points)
VUE_APP.component('vue-users-list', {
    extends: VUE_APP.component('vue-datalist'),
    data() {
        return { search: '' };
    },
    methods: {
        modifyUrlParameters(params) {
            if (this.search) params.push('search=' + encodeURIComponent(this.search));
        },
    },
});
```

**3. The markup** — embed the bundle's template, overriding only the blocks you need:

```
{% embed '@VueInTwig/Widgets/DataList.twig' with { ComponentName: 'vue-users-list' } %}
    {% block TOOLS_LEFT %}

    {% endblock %}

    {% block TABLE_HEADERS %}
        ID
        Name
        Email
    {% endblock %}

    {% block TABLE_ROW %}
        [[ row.id ]]
        [[ row.name ]]
        [[ row.email ]]
    {% endblock %}
{% endembed %}
```

**Props** (on the base `vue-datalist`, inherited by your component)

PropTypeDefaultRequired`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`)

BlockDefaultNotes`TOOLS_LEFT` / `TOOLS_RIGHT`empty / refresh buttonToolbar content`TABLE_HEADERS`empty`` cells, inside the header ```TABLE_ROW`empty`` cells for each `row` in `items``TABLE_BUSY`loading textShown while `isBusy``TABLE_EMPTY`"no results" textShown when `items` is empty`BODY`emptyExtra markup after the table (e.g. a "create" button)`MODAL_CREATE` / `MODAL_DELETE`emptyBody of the create/delete confirmation modals`MODAL_ERROR`error descriptionBody of the error modal**Overridable hooks** (override in your extending component's `methods`)

HookPurpose`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 ```modifyUrlParameters(params)`Push extra query params (e.g. search/filters) before each list request`onItemsRefresh()` / `onItemsRefreshFailure()`Called after a successful/failed refreshCall `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`](#configuration)); `icons`/`texts`/`tooltips` merge per-key, so a partial override keeps the other defaults.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance95

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Total

4

Last Release

25d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/32240357?v=4)[Bruce](/maintainers/Mediagone)[@Mediagone](https://github.com/Mediagone)

---

Top Contributors

[![Mediagone](https://avatars.githubusercontent.com/u/32240357?v=4)](https://github.com/Mediagone "Mediagone (15 commits)")

---

Tags

frontendno-buildphpsymfonytwigvuevue3vuejs

### Embed Badge

![Health badge](/badges/mediagone-vue-in-twig-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/mediagone-vue-in-twig-bundle/health.svg)](https://phpackages.com/packages/mediagone-vue-in-twig-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M400](/packages/easycorp-easyadmin-bundle)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M600](/packages/shopware-core)[symfony/ux-toolkit

A tool to easily create a design system in your Symfony app with customizable, well-crafted Twig components

16126.1k1](/packages/symfony-ux-toolkit)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

1189.8k](/packages/rcsofttech-audit-trail-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
