Skip to content

Commit

Permalink
simplify twig templates + update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
k3ssen committed Apr 29, 2020
1 parent 0a7b454 commit 5894fba
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 99 deletions.
183 changes: 115 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
## SymfonyVuetified

A Symfony 5 project to demonstrate how twig and vue can be mixed:
A Symfony 5 project to demonstrate how Twig and Vue can be mixed.

* Load twig rendered content as Vue-component.
* Pass serverside data into a vue store.
* Render Symfony's FormView as Twig components.
* Load dynamic component over ajax.


### Getting started
## Getting started

Assuming you run a server with php7.4+, mysql, composer, yarn and required modules:

Expand All @@ -25,91 +20,135 @@ Assuming you run a server with php7.4+, mysql, composer, yarn and required modul
For development you can use `yarn watch` to watch javascript/css files.


### Twig-vue-components
## Twig-vue-components


By using dynamic Vue components, rendered twig-content can be used to provide
the template for such dynamic-vue-component.
### Concept

Thanks to Twig's flexible way of dealing with templates it become quite easy to put
a twig block inside a vue object:
```
{% block javascripts }%
{% block script}{% endblock %}
<script>
window.vue = Object.assign({
template: `<div>{{ block('body') }}</div>`,
delimiters: ['{', '}']
}, typeof window.vue !== 'undefined' ? window.vue : {})
</script>
{% endblock %}
```

This will render the content of a body-block inside the template. It is wrapped inside
a div to ensure there's only one root-tag.
Because twig already uses `{{` we redefine the vue-delimiters to only use one `{`.

Furthermore we check if the object has already been defined, so we merge its contents.

Inside Vue, all you need to do is put the object in a dynamic component:

```vue
<component :is="$window.vue"></component>
```

> The `$window` isn't available by default, but it can be easily made accessible by using
`Vue.prototype.$window = window;`.

### Usage

Assuming you've put the part where the body block is put in the vue object inside
the 'base.vue.twig' file you can use it other files by simply extending it:

```
{% extends 'base.vue.twig' %}
{% block template %}
{% block body %}
<p>
{ seconds } seconds have past since this page was loaded.
</p>
{% endblock %}
{% block vueJs %}
window.vue = {
data: () => ({
seconds: 0,
}),
mounted() {
setInterval(() => { this.seconds++; }, 1000);
}
};
{% block script %}
<script>
window.vue = {
data: () => ({
seconds: 0,
}),
mounted() {
setInterval(() => { this.seconds++; }, 1000);
}
};
</script>
{% endblock %}
```

> Note that `window.vue` is purposely used instead of `var vue`. While for this example both have the same effect, using
> the window object makes it easier to redefine its value at different stages.

### Using Fetch

### Dynamic twig form using Symfony's FormView
Because dynamic vue components can be rendered at runtime, the same principles can be used
with `fetch` and load the response in a component.

Symfony's `FormView` can't be directly used in Vuejs, so a `VueForm` class is created to enable serializing the FormView into json.
This can be passed to the `FormType` vue-component where it will render the form, a bit similar to twig's `{{ form(form) }}`.
This project includes a `FetchComponent` that makes it really easy:

Example:
```
<fetch-component :url="/url-to-controller-action"></fetch-component>
```

In your controller action you can pass the form like below:
By utilizing both the flexibility of twig and vue this enables you to load pages
where the body is passed over the window.vue object, but it also enables you
to load only fetch the body for smaller requests.


### Caveats
* Javascript in twig files is not compiled by webpack, so be extra aware of browser-compatibility.
Try to keep javascript complexity to a minimum by using actual Vue components wherever you can.
* No 'a-la-carte' for components in Twig.
Vuetify-loader has a great a-la-carte system that'll automatically import vuetify components that are used in
your own components. Unfortunately this also requires webpack, so it won't work for components that are used
in twig files. You have to make sure to import any required component. You can use the `/assets/js/globalComponents.js`
to make components globally available.
* Unlike components in `.vue` files, dynamic components that are created using twig-rendered data cannot
have scoped styles.
* When using vue and twig mixed together your IDE will probably be less capable of dealing with its content.
TIP: using `.vue.twig` instead of `.html.twig` extensions can help improve IDE support.

## Symfony's FormView as Vue component

Symfony's `FormView` can't be directly used in Vuejs, so a Twig extension is created to enable serializing
the FormView into json.
This can be passed to the `FormType` vue-component where it will render the form, similar to twig's `{{ form(form) }}`.

Example:

```php
return $this->render('admin/user/new.vue.twig', [
'jsonForm' => JsonForm::create($form),
]);
```
Then in your twig file can look like below:
```twig
{% extends 'base.vue.twig' %}
{% block template %}
{% block body %}
<FormType :form="form" />
{% endblock %}
{% block vueJs %}
{% block script %}
<script>
const vue = {
data: () => ({
form: {{ vueForm|raw }}
form: {{ vue_form(form)|raw }}
})
}
</script>
{% endblock %}
```

You can still render parts of the form individually, like this:
To take full control of your form-rendering you can also render parts individually:
```vue
<FormType :form="form.children.name" />
<FormType :form="form.children.email" />
<v-row>
<v-col>
<FormType :form="form.children.name" />
</v-col>
<v-col>
<FormType :form="form.children.email" />
</v-col>
</v-row>
<FormType :form="form" /> <!-- renders remaining form-fields -->
```

### VueStore

Using the VueDataStorage class you can build an array that will be passed
to the vue observable, making this available to all vue components.
By using TwigExtensions, you can easily add data.

This way the previous example could also be achieved like this:

```
{% extends 'base.vue.twig' %}
{% block template %}
{{ vue_store('form', jsonForm) }}
<FormType :form="$store.form" />
{% endblock %}
```


### Custom form-type-components

Expand Down Expand Up @@ -149,15 +188,23 @@ Just have a look at the components in /assets/js/components/Form and you'll noti
complex than the example above.


### Caveats
* Javascript in twig files is not compiled by webpack, so be extra aware of browser-compatibility.
Try to keep javascript complexity to a minimum by using actual Vue components wherever you can.
* No 'a-la-carte' for components in Twig.
Vuetify-loader has a great a-la-carte system that'll automatically import vuetify components that are used in
your own components. Unfortunately this also requires webpack, so it won't work for components that are used
in twig files. You have to make sure to import any required component. You can use the `/assets/js/globalComponents.js`
to make components globally available.
* Unlike components in `.vue` files, dynamic components that are created using twig-rendered data cannot
have scoped styles.
* When using vue and twig mixed together your IDE will probably be less capable of dealing with its content.
TIP: use `.vue.twig` instead of `.html.twig` extensions to improve IDE support.
## VueStore

Using the VueDataStorage class you can build an array that will be passed
to the vue observable, making this available to all vue components.
The `vue_store` added by the twig extension lets you easily add data.

Now rendering a form could also be achieved like this:

```
{% extends 'base.vue.twig' %}
{% block body %}
{{ vue_store('form', jsonForm) }}
<FormType :form="$store.form" />
{% endblock %}
```

The store provides a powerful way of communicating data: even components that are fetched later can alter data
in the store. This can provide nice possibility such as modifying your menu-items or breadcrumbs based on the page
you just fetched.
2 changes: 1 addition & 1 deletion templates/admin/user/delete.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% extends 'base.vue.twig' %}
{% set title = 'User' %}

{% block template %}
{% block body %}
{{ form_start(form) }}
{{ form_widget(form) }}
<v-btn type="submit" color="danger">
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/user/edit.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% extends 'base.vue.twig' %}
{% set title = 'User' %}

{% block template %}
{% block body %}
{{ vue_store('form', vue_form(form)) }}
<v-form action="{{ path('admin_user_edit', user) }}" method="post">
<form-type :form="$store.form"></form-type>
Expand Down
4 changes: 2 additions & 2 deletions templates/admin/user/index.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
{% extends 'base.vue.twig' %}
{% set title = 'User' %}

{% block template %}
{% block body %}
<datatable :datatable="datatable"></datatable>
<v-btn href="{{ path('admin_user_new') }}" color="success">Add user</v-btn>
{% endblock %}

{% block vueJs %}
{% block script %}
<script>
window.vue = {
data: () => ({
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/user/new.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% extends 'base.vue.twig' %}
{% set title = 'User' %}

{% block template %}
{% block body %}
{{ vue_store('form', vue_form(form)) }}
<v-form action="{{ path('admin_user_new', user) }}" method="post">
<form-type :form="$store.form"></form-type>
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/user/show.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
{% extends 'base.vue.twig' %}
{% set title = 'User' %}

{% block template %}
{% block body %}
Username: {{ user.username }}
{% endblock %}
6 changes: 2 additions & 4 deletions templates/ajax-base.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
<div>
{% block appContent %}
{% include 'flash_messages.html.twig' %}
{% block body %}
{% block template %}{% endblock %}
{% endblock %}
{% block body %}{% endblock %}
{% endblock %}
</div>
</template>

{% block javascripts %}
{% block vueJs %}{% endblock %}
{% block script %}{% endblock %}
<script>
window.vue = Object.assign(
{ delimiters: ['{', '}'] },
Expand Down
20 changes: 7 additions & 13 deletions templates/app-base.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,19 @@
<v-breadcrumbs :items="$store.breadcrumbs" divider=">"></v-breadcrumbs>
</div>
{% include 'flash_messages.html.twig' %}
{% block body %}{% endblock %}
</app>
{% endblock %}

{% block javascripts %}
{% block vueJs %}{% endblock %}
{% block script %}{% endblock %}
<script>
{% if block('template') is defined %}
var baseVue = {
window.pageVue = Object.assign(
{
template: `<div>{{ block('body')|replace({'`' : '\\\`'})|raw }}</div>`,
delimiters: ['{', '}'],
template: `<div>{{ block('template')|replace({'`' : '\\\`'})|raw }}</div>`,
};
{% endif %}
if (typeof baseVue !== 'undefined') {
window.pageVue = Object.assign(
baseVue,
typeof window.vue !== 'undefined' ? window.vue : {}
);
}
},
typeof window.vue !== 'undefined' ? window.vue : {}
);
window.store = {{ get_vue_store() | raw }};
</script>
{{ parent() }}
Expand Down
4 changes: 2 additions & 2 deletions templates/dashboard/index.vue.twig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{% block title %}TwigVue component{% endblock %}

{% block template %}
{% block body %}
{{ vue_store('menuItems', [{text: 'SomeMenuText', href: '#testen', icon: 'mdi-view-dashboard'}]) }}
<div class="example-wrapper">
<h1>Twig and Vue</h1>
Expand Down Expand Up @@ -95,7 +95,7 @@

</div>
{% endblock %}
{% block vueJs %}
{% block script %}
<script>
let vue = {
data: () => ({
Expand Down
2 changes: 1 addition & 1 deletion templates/library/delete.vue.twig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends 'base.vue.twig' %}
{% set title = 'Library' %}

{% block template %}
{% block body %}
{{ form_start(form) }}
{{ form_widget(form) }}
<v-btn type="submit" color="danger">
Expand Down
2 changes: 1 addition & 1 deletion templates/library/edit.vue.twig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends 'base.vue.twig' %}
{% set title = 'Library' %}

{% block template %}
{% block body %}
<h1>
Edit library
</h1>
Expand Down
2 changes: 1 addition & 1 deletion templates/library/index.vue.twig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends 'base.vue.twig' %}
{% set title = 'Library' %}

{% block template %}
{% block body %}
{{ vue_store('datatable', datatable) }}
<datatable :datatable="$store.datatable"></datatable>
<v-btn href="{{ path('library_new') }}" color="success">Add library</v-btn>
Expand Down
Loading

0 comments on commit 5894fba

Please sign in to comment.