In-depth guide: Building Vue.js single page application on Pimcore
Pimcore has become the center of our development and we’re discovering all the features offered by Pimcore and Symfony framework.
Moreover, when discussing frontend, we’ll point out that Vue.js is one of the growing frontend frameworks. It’s been used in creating web interfaces and single page applications – and that’s what we’ll show you today! We’re a certified Pimcore Silver partner and a professional Pimcore development agency, so it’s safe to say our methods are nothing less than successful!
Continue reading and find out how to create a Vue.js single page application in Pimcore!
What we have prepared for you
- Symfony/webpack-encore
- Coding in Pimcore/Symfony
- Building a Vue.js application
- Looking for more information?
Symfony/webpack-encore
Before we start, take some time to get to know more about Pimcore. To make learning Pimcore as easy as possible, check out one of our previous articles where you’ll find examples and explanations for beginners.
And now – let’s start!
We can look at Webpack Encore as a wrapper around webpack which gives us API for bundling JavaScript modules, pre-processing CSS & JS and compiling and minifying assets.
Encore is inspired by Laravel’s mix and it can be used by its own (outside of Symfony applications), eg. on WordPress applications with some minor tweaks in setup.
Installation
First things first, we need to install WebpackEncoreBundle via composer:
This will install everything needed for webpack connection to our Pimcore/Symfony application, eg. twig helpers which will read manifests and know how to handle versioned files provided by webpack.
For those helpers to know where to find our bundled files, we need to update configuration; go to app/config/config_dev.yaml and add:
It’s quite important to keep the “/../web/” part, since Pimcore is using a web folder by default and we want to serve our content from there, while “build” is our wanted destination folder of webpack outputs.
Next, create .env file in project root folder (if there isn’t any) and add your vhost:
This will be used by our webpack configuration in later parts.
After composer packages, we need to install missing javascript modules for webpack encore to use.
If you don’t have an existing package.json, run: “npm init -y” which will create one.
Run this command to install webpack-encore:
After this, we need to install some additional packages:
Each of these packages will be used either in our webpack configuration or in Vue.js application source code.
* Npm init -y will create a package.json without any questions asked (default git repo & readme will be used)
* We installed sass-loader package version @7.3.1, since version 8 needs some extra workarounds to work with webpack-encore.
Webpack configuration (webpack.config.js)
We have everything needed to start with our webpack configuration. Create “webpack.config.js” in the project root folder. This file will contain all configuration for our bundler. Let’s add full configuration now, and we will go through its main components later.
If we require dotenv like this we will have our .env file parsed and set inside process.env; with this we can use our previously set HOSTNAME from .env file in our config.
*This is quite useful since not every person in the team has the same virtual host set for the project and we shouldn’t hardcode that.
Asset entries & outputs, SASS/SCSS support
It’s time to create our folder structure:
With “setOutputPath()” we set a directory where our compiled assets will be stored, while with “setPublicPath()” we set a public path used by the web server to access the output path.
“Assets” folder is quite self-explanatory, we will have our assets source here, while the “build” folder will serve as a webpack output (again, take care that output needs to be in web folder since Pimcore is using it by default and we want to serve it from there).
With “addEntry()” we actually add a javascript entry point for our application. Each entry will result with one javascript and one css file (eg. app.js and app.css). First parameter, in our case “app” is important, since it is used as some kind of namespace with provided twig helpers, which we will take a deeper look in next chapter.
With “enableSassLoader()” we actually enable SASS/SCSS support and we “turn on” sass/scss files loader in webpack (this is the default part of webpack-encore).
We will use default settings except “resolveUrlLoader: false”, which will tell webpack to keep every url (fonts, images…) in code intact (it won’t resolve them).
*There is also “addStyleEntry()” function which works the same as “addEntry”, but it accepts sass/scss file as entry and we can use output in the same way via “namespace” and twig helper. In our example app, we will import/bundle styles along with our app’s javascript code.
*Please note that we can have many “addEntry” functions, and with that many separated bundled files, we can have many “per page” javascript files or many separate applications!
Extra ES6 babel presets and Vue.js support
While babel is automatically configured for usage with .js files and ECMA2015/ES6 via “@babel/preset-env”, let’s say we want something “extra”, maybe JSX syntax in Vue’s component render function, or ES7’s async/await cool functionality with which you can “escape” promises callback hell?
Well, you can add them via “configureBabel()”, but take care what you wish to use, presets or plugins. Also, with an extra param you can ignore folders which won’t be processed with babel.
*“Node_modules” are ignored by default, they are included only for presentation purposes.
With “enableVueLoader()” we enable Vue.js support; loader for .vue files (vue-loader, vue-template-compiler etc..). Thanks to default configuration of webpack encore, this is all config we need.
Webpack Dev server, Hot module reload (HMR), Browsersync plugin
So far we have set everything needed for webpack to create output bundles from our source code. But our final goal is to get as much “automation” as we can get in our development process. Thankfully with webpack’s dev server, we can achieve most of it.
What is actually happening here is that we set up a web server on localhost:8080 which will serve our current state of webpack output files. Our Pimcore application is requesting them via encore twig helpers. On every change, after the webpack handles those changes, he will trigger HMR (Hot module reload) and inject newest changes via connected socket (without browser reload), and that’s pretty neat 🔥.
Ok, now we have the dev server up and running and it’s all working with our javascript & stylesheet files, but what if there is a specific case when you want to watch your .php or .twig files? This can be done easily since there is a browsersync plugin for webpack.
What happened now is that we added an extra layer on our Pimcore application which is already connected to the running webpack dev server with HMR. We actually just proxied our app and told browsersync to watch some files (via “files: []” option) on which changes it will trigger full page reload.
Like this, any change in our javascript or sass will trigger HMR, while .twig or .php file changes will trigger full page reload.
*With “Encore.getWebpackConfig()” we are extracting the current configuration in which we can now insert some of our extra config and later export it overridden.
*With ignored “node_modules” in watchOptions we tell webpack to ignore them from his watchlist, and we can save some of the system memory like that.
NPM scripts, split entry chunks, source maps & versioning
Till now we should have everything properly installed and setuped, what’s missing are our npm scripts; add this snippet to your package.json:
Each of the scripts can be runned via command: “npm run {scriptName}”.
Let’s go through each of them:
- “npm run watch” will fire up webpack and browsersync along with it but without HMR, webpack will change bundled files, but reload “by hand is needed” to preview changes
- “npm run hot” will fire up dev server with HMR activated along with browsersync and this behaviour gives us most of automation in our development process
- “npm run build” will trigger the production script; js and css files are minified and created in the output folder (“web/build”). Consider this as an “deploy” script which will also handle file versioning and you’ll probably run it on server
With the “splitEntryChunks()” option we tell webpack to split our output bundle files into smaller pieces (chunks) for greater optimization (that extra files urls are also handled with encore’s twig helpers). By default webpack spits out an extra file called “vendor” which contains our imported packages eg. Vue.js, jQuery… This “magic” is done with webpack’s “SplitChunksPlugin” under the hood.
While bundling our assets is a great thing, debugging after that process can be quite hard. Let’s say you want to inspect and find in which sass file you created some style and everything you got is a line in your “custom.min.css”… This can be avoided by using source maps which will tell us the name of our true source file. With “enableSourceMaps(!Encore.isProduction())” we activate that feature, but only for development environments, since we don’t want them to be visible in production state.
If you have an extra time to spend, you can read everything connected to source map.
While Pimcore by default has great cache busting capabilities, it’s easier for us that we change file hashes on our changes via our bundling system.
With “enableVersioning(Encore.isProduction())” we activate filename hashes only for production environments since we have HMR when developing.
Coding in Pimcore/Symfony
Now we should have everything properly configured, it’s time for coding!
Document, controller action
To have proper SPA setup, let say that we need to always render our twig template first, in which we have our Vue.js application mounted. After that our frontend (Vue) router will handle all the routing and we don’t want any conflicts with Symfony after that.
Let’s go through a few approaches to handle this;
We can actually create one controller with route annotation and all routes will be handled by Vue with this annotation.
This controller will render index.twig.html, but as you can see we excluded some routes, eg. “/admin” routes are “reserved” for Pimcore administration, “/api” routes will be used by our Vue application and “/site” can be used for serving some extra content.
Take care that with this approach you will need to handle 404 pages via Vue.js router application logic.
*Since this approach isn’t fully tested on every part of Pimcore administration, I’m not 100% sure that this isn’t in conflict with some of the administration routes.
Some other approaches could be that we use this controller action (without route annotation) and set it to documents in Pimcore administration. Also for some static contents (eg. product pages) we can create additional routes that will also render the same twig template.
Both approaches are great if you are willing to create an API route for fetching document content via documentID and some kind of extra data mapping from Pimcore editables. Also like this, we can follow proper documents hierarchy in our SPA application like we would in our “classic” setup.
An important note is that we need to send some document data to view (like title, description, metas etc. which we will render through twig) and also a JSON object which will contain document structure (for the 2nd mentioned approach).
* We won’t write any more backend code here since this shouldn’t really be a “real” part of our guide 😏.
Twig helpers
Let create our index.html.twig and create something like this:
It is quite important that we render title and meta via twig helpers since it is rendered with HTML and we don’t need to bother with crawlers and robots since they will read this just fine.
* Even today, SEO is a concerning problem for many SPA applications.
“appConfig” global object will store some info needed for the app (like locale etc).
We create an empty div in which we will mount our whole Vue.js application (“app-root”).
Encore_entry_link_tags() and encore_entry_script_tags() are helpers from Symfony/webpack-encore package which are used for fetching proper URLs to our bundler outputs. Remember the “app” namespace that we set in our webpack.config.js? This tells the helper to use this Encore entry.
But besides that, helpers are using “entrypoints” and “manifest” JSON files which are webpack outputs. Like this, we can use bundle outputs regardless of our environment, and also splitted output chunks which is really neat :).
Building a Vue.js application
Like we said before, for our app to behave like real Single Page Application (SPA), we need to cover these requirements:
- Single page applications are built in JavaScript (Vue)
- After initial load, all page routing needs to be done via frontend (Vue router)
- After initial load, we will handle everything else via ajax request (axios + Vuex)
Ok, let’s go to work! 😀
*We won’t take a deep dive into Vue router and Vuex, since their behaviours and concepts can be blog posts on it’s own.
Folder structure
First we need to extend our existing folder structure;
Components folder will have Vue components which we will register globally in our main mounted app (eg. Header.vue and Footer.vue).
Store folder will have Vuex initialization and all it’s connected modules with actions.
In the utils folder we will have things that we need for some “external” logic, like API helpers, routes configuration, Vue.js custom filters…
Views folder will have our “views” or “pages” components which will be rendered by our Vue router.
Application entry point
Our Vue.js application entry point is app.js, also it’s entry for our Encore bundler.
Here we have some code written inside it:
As you can see, we import our Vuex store and global components, we are creating router instance and also custom VueFilters.
Here we also import our sass file so webpack can extract compiled css file.
In the end, we create a Vue app, connect it to the router and store, and register some global components.
*We’re exporting the App instance only for some future cases (if needed).
*We won’t cover .sass files and stylings, since this shouldn’t really be a part of this guide.
Routing – Vue Router
Application router entry point will be router.js. Let’s configure our router like this:
We are creating our Router instance this way, with function, so we can send some of the routes through our functions as params or maybe even create a new router instance sometime after dynamically through code.
By default Vue router uses hash mode (it uses hashes to simulate full urls) which is “ugly”. To get rid of the hashes we use the router’s history mode, which uses the history.pushState API to achieve URL navigation without a page reload.
Important thing is that, for link usage we will use a “router-link” component which enables user navigation in router-enabled applications.
This will render the “<a>” tag with correct href by default via “to” prop.
*Routes configuration will be covered in components & utilities part.
Vue Store – Vuex
“Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application”
- description of Vuex in it’s official documentation.
Maybe much easier to understand is that we call it “data store”, a place for data that needs to be shared between components, eg. a “single source of truth”. Components must read application data from this location and not keep their own copy to prevent conflict or disagreement.
If you ever used something like “redux” or “flux” you will find this quite similar, but to keep going with this guide, please take care that you should know some fundamentals of Vuex. If not, core concepts have great explanations in official documentation.
With that on our mind, let’s create folder structure like this inside our “store” folder;
Now it’s time to actually create our store, add this to index.js:
Since in Vuex we have a “single state tree” all of our application states are contained in one big object, and as our application grows our store can get bloated. That’s the reason why we’re using modules from the beginning, which can behave like it’s own separate store.
Let’s create our module in productData.js:
Here we created our module state, it has “data” which will be sent to components and propagated via props if needed. Also there is an “isLoaded” flag which tells us the current state of our request and we can use it in our component (eg. for showing loaders).
Main thing is the “getProductData()” async function, which, when dispatched, fetches response from the axios api helper and sets data. Also this is “good time” to set document title via javascript.
* In our example app, we have an API route which returns us Pimcore product Object content via product ID.
* When “namespaced: true” is set, we tell our store that our getters, actions and mutations are only available under this path, not globally, which they are by default.
Components & utilities
Finally, let’s add missing files to our folder structure:
For our router to work, let’s finally create routes configuration, add this to routes.js:
For simplicity, we have only two components that represent views (“HomePage” and “ProductPage”).
ProductPage will have a “connection” to store, and action will be dispatched in the “beforeEnter” navigation guard. An important thing to mention is that beforeEnter will be triggered before component render.
Let’s also create api.js helper:
Here we’re using an awesome “axios” http library with async/await combination for fetching data from our api endpoint.
Take care that “fetchProductData()” is used inside getProductData() Vuex action.
Lastly, let’s create some custom Vue filters, add this to vueFilters.js:
This filter is simple and it’s used for presentation purposes only, but I think it’s quite enough to figure out how to create and “register” other ones now.
Custom Vue filters are used same as twig filters with “|filter_name” in templates.
Finally we need to create the last thing in our application, create missing components files.
We’re using Vue.js “Single File Components” with .vue file extensions. I think that there is no real need to cover part of writing components in this guide, since official documentation has it covered just fine.
But, we will cover our component’s connection to Vuex store. We’re using computed properties in which we will map our data from the store.
Add something like this to “ProductPage.vue”:
We’re using the “mapState” helper from Vuex package which generates getter functions for us, which will save us much time on code writing. While this is maybe a bit “cleaner” approach you can also use “this.$store.state” to fetch data since the store is already injected in all child components of our application since we already connected the root component to it.
With this covered you can use computed properties in your component template like you would normally.
* Remember that with babel preset (“@vue/babel-preset-jsx”) you can write “react-like” templates using jsx syntax inside your render function without using <template> tags inside the .vue file
To wrap it all, go back to the index.html.twig and add vue.js registered components to our app root node;
We’re always rendering footer and header, while components matched by router will be rendered inside “router-view”, which is a functional component provided by Vue router itself.
Looking for more information?
While this guide is maybe a bit “overwhelmed” with so many web technologies that were used, I think as a developer you should now have a better picture of how Single Page Application works and how you can actually build one on Pimcore systems.
With this guide and all code samples inside it, I think that in the end, we have a useful boilerplate for creating Vue.js SPAs on the top of the Symfony/Pimcore systems.
Improvements – GraphQL, Vue Apollo
Since we used a “classic” API approach with axios, there is a newer tech awaiting to be implemented, called GraphQL.
GraphQL is a syntax or rather a query language that describes how to ask for data and is generally used to load data from a server to a client. With GraphQL, the user is able to make a single call to fetch the required information rather than to construct several REST requests to fetch the same.
Pimcore has a “Data Hub” tool which has GraphQl support integrated along with GraphiQl as GUI; check it here on Pimcore’s official website if you haven’t’ checked it yet! With it, you can connect GraphQl with your already existing data objects.
Concerning frontend, there is Apollo client for GraphQl, which has its own Vue.js implementation.
GraphQl is new and fancy, and he comes with so many benefits, but maybe it will be a theme for some other guide. 🙂
So much from me, I hope you like this guide and bye till some other time?
Author: Alen Egredžija, Frontend developer