DEVELOPMENT by Pedro Resende

Symfony with Vue.js frontend Integration

with tailwindcss

Post by Pedro Resende on the 6 of February of 2020 at 10:30

A few years ago, I've dedicated a post to the integration of React.js with Symfony (Symfony with React.js frontend Integration).

For the last year and a half I've been working on a daily basis using Vue.js has a dedicated frontend framework. I must say that when I started, I was against it since I had already quite some experience with React.js and it was strange, however I have to admit it was a good surprise. The learning curve is way better than React.js, and when comparing Redux with VueX it's amazing how simple it is to use.

Therefor, I've decided, like I've done 4 years ago with React.js to write a possible integration with Symfony.

A kind reminder that this is a possible integration and if any of you believe there is a better approach, please feel free to leave a comment.

Let's start by installing a clean Symfony 5.x environment for this example:

$ symfony new vuejssymfonyintegration

Let's start by installing twig integration in order to have a frontend

$ cd vuejssymfonyintegration
$ composer req twig
$ symfony serve

Access to your browser, using the address provided, probably something like

[OK] Web server listening on https://127.0.0.1:8000 (PHP CLI 7.4.2)

Let's start by creating a controller to return our main page

$ composer req make doctrine/annotations symfony/webpack-encore-bundle
$ ./bin/console make:controller

Back to the editor, now you'll see the presence of `VueFrontEndController.php` on the folder `src/Controller`. If you open your file, you'll have something like

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class VueFrontendController extends AbstractController
{
    /**
     * @Route("/vue/frontend", name="vue_frontend")
     */
    public function index()
    {
        return $this->render('vue_frontend/index.html.twig', [
            'controller_name' => 'VueFrontendController',
        ]);
    }
}

Let's change the Route from "/vue/frontend" to "/" in order to show it has our homepage.

Now, let change the contents of the file `templates/vue_frontend_index.html.twig` which has the following:

{% extends 'base.html.twig' %}

{% block title %}Hello VueFrontendController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ '/home/pedroresende/Projects/vuejssymfonyintegration/src/Controller/VueFrontendController.php'|file_link(0) }}">src/Controller/VueFrontendController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/home/pedroresende/Projects/vuejssymfonyintegration/templates/vue_frontend/index.html.twig'|file_link(0) }}">templates/vue_frontend/index.html.twig</a></code></li>
    </ul>
</div>
{% endblock %}

to something like

{% extends 'base.html.twig' %}

{% block title %}Hello VueFrontendController!{% endblock %}

{% block stylesheets %}
    {{ encore_entry_link_tags('app') }}
{% endblock %}

{% block body %}
    <div id="app"></div>
{% endblock %}

{% block javascripts %}
    {{ encore_entry_script_tags('app') }}
{% endblock %}

Now that we have our Symfony structure ready, let's go into the JS part.

Let's add encore.js to our project, by running

$ composer req symfony/webpack-encore-bundle
$ yarn install
$ yarn add vue tailwindcss --save
$ yarn add @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props vue-loader@^15.0.11 vue-template-compiler postcss-loader@^3.0.0 sass-loader@^7.0.1 node-sass --dev

Open webpack.config.js file, which is located at the root of our project and change to have the following:

var Encore = require('@symfony/webpack-encore');

if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .addEntry('app', './assets/js/app.js')
    .enableSingleRuntimeChunk()
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    .enableVersioning(Encore.isProduction())
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })

     .enableVueLoader(() => {}, {
         useJsx: true
    })
     .enablePostCssLoader()
     .enableSassLoader(function (options) {}, {
        resolveUrlLoader: false
    })
;

module.exports = Encore.getWebpackConfig();

Let's add a postcss.config.js file, with the following contents

module.exports = {
  plugins: [
    // ...
    require('tailwindcss'),
    require('autoprefixer'),
    // ...
  ]
}

Open /assets/app.js file and change it to

import '../css/app.css';

import Vue from 'vue';
import vuefrontend from './../vue/vuefrontend';

if (document.getElementById('app') !== null) {
  new Vue({
    el: '#app',
    template: '<vuefrontend/>',
    components: {
        vuefrontend
    }
  })
}

console.log('Hello Webpack Encore! Edit me in assets/js/app.js');

Let's create the file assets/vue/vuefrontend.vue with the following content

<template>
    <div class="container m-auto">
        <div class="p-20">
            {{ message }}
        </div>
    </div>
</template>

<script>
export default {
    name: 'vuefrontend',

    data() {
        return {
            message: 'Hello World'
        }
    }
}
</script>

Let's boot the encore watcher, to take into account our changes

$ yarn encore dev --watch

Back to your browser, you should see Hello World

That is it, please let me know if you propose a different approach, the code is available at this link.