Blogs

Blogs

Build a complex VueJS app with Liferay 7.x

Since the arrival of the FixPack 30 Liferay 7, it is possible to use NPM as a Javascript dependency manager. On this occasion, Liferay added to their template module generator, Blade, the possibility to create portlets that use recent JS framework such as Angular, React or VueJS.

We will aim to create a simple Todo List application with VueJS , where entries on our list will be persisted in a database through services incurred by Liferay.

Regarding the services layer, we use the service builder. The goal here is not to detail this section, we start from the premise that we have the following services exposed:

  • addTodo(String) will allow us to add an entry with the content of the task as parameter

  • getTodos() will allow us recover our list of tasks

  • setDone (boolean) will allow us to define whether a task has been achieved or not

 

Let's create our VueJS portlet !

Nothing is easier with Blade:

blade create -t  npm-vuejs-portlet -p fr.ippon.demo todo-web-vuejs

Ok, let’s have a look !

The generated starter is very light.

It creates our Vue component instance root (new Vue)... and that's all !

import Vue from 'vue/dist/vue.common';

export default function(elementId) {

 new Vue({

    el: `#${elementId}`,

    data: {

       text: 'Hello World!',

    },

 });

}

The most annoying thing is the lack of build tools to work with Single File Component (SFC) in order to mix template, logic and style in a single file, which is the minimum of what we could expect when we decide to work with VueJS.

Working with SFC components is made possible by tools like webpack or Browserify.

So, the goal of this development will be to build a VueJS portlet with the capability to work with SFC files, keeping the possibility to pass the portlet namespace during instantiation (to keep the partitioning of our code if the portlet is instantiable) and fully industrializable.

webpack.config.js

First of all, we will create a webpack.config.js file at the root of our project.

This config file is a place to put all of our configuration, loaders (explained later), and other specific information related to our build.

First, declare the entry point of our application and the output file (all components of our application will be "packaged" into a single file by webpack, with all the dependencies )

var path = require('path')

var webpack = require('webpack')

module.exports = {

entry: './src/main/resources/META-INF/resources/js/index.js',

output: {

  path: path.resolve(__dirname, './build/resources/main/META-INF/resources/dist/'),

  filename: 'build.js'

},

module: {

// Here will be the configuration of our loaders

}

}

The entry point of our application will be the index.js file and webpack will produce a build.js file as the output in the build/resources/main/META-INF/resources/dist/ folder.

We will delegate the build task to webpack.

Let’s go back to our package.json file.

We will delegate the transformation of our Node.js modules to a webpack script, so in our scripts section, we will add:

"postinstall": "webpack"

We already have specified our entry point into our webpack script, so the next line is no longer needed:

"main": "js/index.js",

We will add a dependency webpack-cli to execute our webpack script

For now, our package.json file is greatly simplified and should look like this:

{

 "dependencies": {

    "vue": "2.4.4"

 },

 "devDependencies": {

    "babel-cli": "6.26.0",

    "babel-preset-es2015": "6.24.1",

    "babel-preset-liferay-project": "1.6.1",

    "liferay-npm-bundler": "1.6.1",

    "liferay-npm-bundler-preset-vue": "1.6.1",

    "webpack-cli": "2.0.15"

 },

 "name": "todo-web-vuejs",

 "scripts": {

    "build": "babel --source-maps -d build/resources/main/META-INF/resources src/main/resources/META-INF/resources && liferay-npm-bundler",

    "postinstall": "webpack"

 },

 "version": "1.0.0"

}

So now, we have to change the default view of our portlet (view.jsp) to load the file built by webpack (our build.js file declared as the output).

Let’s have a look to our jsp file.

<aui:script require="todo-web-vuejs@1.0.0">

 todoWebVuejs100.default('<portlet:namespace />');

</aui:script>

Liferay, in its build tasks, create one that takes care of packaging all javascript dependencies, like webpack, but in an OSGi’s context called liferay-npm-bundler. The problem is that it doesn’t allow us to work with .vue files as we already said.

In our package.json file, we will delete the "&& liferay-npm-bundler” command that will duplicate webpack build tasks (but without the benefits of it).

Let's simplify our package.json file. We can now delete the following lines that have become useless:

"babel-preset-liferay-project": "1.6.1",

"liferay-npm-bundler": "1.6.1",

"liferay-npm-bundler-preset-vue": "1.6.1",

And from our JSP file :

<aui:script require="todo-web-vuejs@1.0.0">

 todoWebVuejs100.default('<portlet:namespace />');

</aui:script>

Instead, we will add a link to our output packed file:

<script src ="/o/todo-web-vuejs-1.0.0/dist/build.js"></ script>
In an OSGi context, the /o matches the context of the container and then, the naming convention is /<bundle-name>-<version>

As it stands, if we put this statement in our view.jsp file, it won’t work. In fact, there is an entry in our bnd.bnd file (responsible for the generation of the MANIFEST.MF) that overrides this naming convention:

Web-ContextPath: /todo-web-vuejs

If we want this to work, we should write

<script src ="/o/todo-web-vuejs/dist/build.js"></ script>

But, as we want to do things the right way, we want to keep the versioning of our portlet to stay in a pure OSGi context, so we will replace the following line :

Web-ContextPath: /todo-web-vuejs

by

Include-Resource: package.json

This way, the naming convention will be brought by our package.json file and the path to our sources will now be dependent of the fields name and version of our package.json file

At this stage, our bnd.bnd file must be:

Bundle-Name: todo-web-vuejs

Bundle-SymbolicName: fr.ippon.demo

Bundle-Version: 1.0.0

Export-Package: fr.ippon.demo.constants

Include-Resource: package.json

and our view (view.jsp)

<%@ include file="/init.jsp" %>

<script src="/o/todo-web-vuejs-1.0.0/dist/build.js"></script>

<div id="<portlet:namespace />">{{text}}</div>

We will have to replace the javascript initialization block formerly carried by the tag <aui:script> that has been deleted.

We will rewrite our index.js to export our initialization function called main

import Vue from 'vue/dist/vue.common';

const main = (elementId) => {

  new Vue({

      el: `#${elementId}`,

      data: {

          text: 'Hello World!',

      },

  });

};

export { main };

In our JSP, we'll have to call our initialization method, something like

<script>

  window.onload = function () {

      myEntryPoint.main('<portlet:namespace />');

  };

</script>

The problem is that, after minification / obfuscation sources by webpack, our entry point could have any name. So We will add two properties in the output section of our webpack.config.js file to solve this problem.

libraryTarget: 'var' (Optional because this is the default value)

This specifies how our library will be exposed (here in a variable)

library: 'entryPoint'

The name that we want to give to our variable. The output of our webpack.config.js file should now look like this:

output: {

  path: path.resolve(__dirname, './build/resources/main/META-INF/resources/dist/'),

  filename: 'build.js',

  libraryTarget: 'var',

  library: 'entryPoint'

}

So now, we can write in our JSP:

<script>

  window.onload = function () {

      entryPoint.main('<portlet:namespace />');

  };

</script>

At this point, we should have a functional portlet that does nothing more than the generated portlet based on blade template, but where the build jobs have been fully delegated to webpack

We will be able to work directly with .vue files and much more !

Working with .vue files

Let's create a folder named components that will contain… Yes ! Our components. We will create a component called TodoApp that will be our Todo List

In js/components/ create a TodoApp.vue file.

In our index.js, we are going to call our component. In order to do that, we import it (1), we insert our todo app in the template of our component (2) and we add our component in  the dependence of our view (3)

import Vue from 'vue/dist/vue.common';

import TodoApp from "./components/TodoApp.vue"; (1)

const main = (elementId) => {

  new Vue({

      el: `#${elementId}`,

      template: `

          <div>

              <h1>My beautiful ToDo App</h1>

              <todo-app /> (2)

          </div>

      `,

      components: {

          TodoApp (3)

      }

  });

};

export { main };

We will be able to develop our component view with some templating, a script part and a css one.

<template>

  <div class ="todo-app">

      <input

         placeholder = "Add a to-do"

      />

      <ol>

          <li v-for = "(item i) in items">

              {{item.description}}

          </ li>

      </ ol>

  </ div >

</ template>

<script>

  // Here our scripts

</ script>

<style>

  / * Here our CSS * /

</ style>

Then we will have to declare our loaders in our webpack configuration to process our component view, and therefore our scripts and css.

For that, we declare rules to treat a type of file by a loader

From the documentation: “Loaders allow you to preprocess files as you require() or “load” them. Loaders are kind of like “tasks” are in other build tools, and provide a powerful way to handle frontend build steps. Loaders can transform files from a different language like CoffeeScript to JavaScript, or inline images as data URLs. Loaders even allow you to do things like require() css files right in your JavaScript!”

First, we declare a loader for our .vue files

...

module: {

rules: [

      {

          test: /\.vue$/,

          loader: 'vue-loader'

      }

  ]

}

...as well as its development dependencies in our package.json

"vue-loader": "14.2.1"

We also need vue-template-compiler

"vue-template-compiler" : "2.5.16"
Note: Take this opportunity to update the version of our vue dependency to the latest one.
"vue": "2.5.16"

To complete this demonstration, we will add a typescript loader if we decide to write our components in this language and a css precompilation loader if we want to make more advanced style sheets.

{

  test: /\.(ts|tsx)$/,

  loader: 'ts-loader',

  exclude: /node_modules/,

  options: {

      appendTsSuffixTo: [/\.vue$/],

  }

},

{

  test: /\.scss$/,

  use: [{

      loader: "style-loader"   }, {

      loader: "css-loader"   }, {

      loader: "sass-loader"

  }]

}

We will add our dependencies to our package.json file

"css-loader": "^0.28.10",

"node-sass": "^4.7.2",

"sass-loader": "^6.0.6",

"style-loader": "^0.20.2",

"ts-loader": "4.0.0",

"typescript": "^2.3.2",
Note: As we decide to work with typescript modules, we will need to add a tsconfig.json file at the root of our project that will contain the path to the sources to be transpiled and the options for it.

An example of code to this address with:

  • Components in typescript for writing

  • CSS part SASS

https://gitlab.ippon.fr/acharpentier/todo/tree/master/todo-web-vuejs

Hope this tutorial will help to build complex applilcations with VueJS

Regards

Arnaud