Vue.js

Vue.js is an open-source JavaScript component-based framework that makes it easy to develop reactive applications.

Here you will find some guidelines explaining how you have to proceed to build up you application.

Before beginning

Vue.js can be used along with some additional libraries like VueX or Vue-router. To know when you have to use these libraries, here is a table that will help you to decide:

Your app

Which lib should I use?

Has a component with few responsibilities

Vue.js

Is medium-sized and has complex workflows

Vue.js + Vuex

Has several pages to display

Vue.js + Vuex + Vue-router

Note

We strongly suggest you to install the vue-devtools browser extension. It provides nice features that ease the development of your applications.

Folder structure of a Vue application

A Vue app has to be split out in distinct parts.

Here is the folder structure you have to follow:

my-plugin/
    |-- build-manifest.json               # Edit it to declare your app for the build system and for translations
    |-- scripts/
         |-- my-vue-app/
              |-- po/                     # Localization strings
              |-- src/                    # The app source-code
                   |-- api/               # REST API consumers
                   |-- components/        # Vue components
                        |-- MyFeature/
                        `-- OtherFeature/
                   |-- store/             # Vuex store modules (actions/mutations/getters)
                   |-- router/            # Vue-router modules
                   |-- index.ts           # App bootstrapping
         |-- jest.config.js               # Unit tests bootstrapping
         |-- package.json                 # Declares the App, its dependencies and its build script.
         |-- package-lock.json            # Generated by npm. Never edit manually.
         |-- tsconfig.json                # Typescript configuration
         `-- webpack.config.js            # Webpack configuration to build the App

Build your Vue application

The build system will read build-manifest.json to understand where it needs to run npm install and npm run build.

// tuleap/plugins/my-plugin/build-manifest.json
{
    "name": "my-plugin",
    "components": [
        "scripts/"
    ],
    "gettext-vue": {
        "my-vue-app": {
            "src": "scripts/my-vue-app/src",
            "po": "scripts/my-vue-app/po"
        }
    }
}

To build up your application, you will have to update or create a webpack.config.js file. This file should be located in my-plugin/scripts/.

// tuleap/plugins/my-plugin/scripts/webpack.config.js
const path = require("path");
const webpack_configurator = require("../../../tools/utils/scripts/webpack-configurator.js");

const assets_dir_path = path.resolve(__dirname, "./assets");

const webpack_config_for_my_awesome_vue_app = {
    entry: {
        "my-vue-app": "./my-vue-app/src/index.ts"
    },
    context: path.resolve(__dirname),
    output: webpack_configurator.configureOutput(assets_dir_path),
    externals: {
        tlp: "tlp"
    },
    module: {
        rules: [
            ...webpack_configurator.configureTypescriptRules(webpack_configurator.babel_options_ie11),
            webpack_configurator.rule_easygettext_loader,
            webpack_configurator.rule_vue_loader
        ]
    },
    plugins: [webpack_configurator.getManifestPlugin(), webpack_configurator.getVueLoaderPlugin()],
    resolveLoader: {
        alias: webpack_configurator.easygettext_loader_alias
    }
};

module.exports = webpack_config_for_my_awesome_vue_app;

Once you have a webpack config, you will need a package.json in my-plugin/scripts/.

// tuleap/plugins/my-plugin/scripts/package.json
{
  "author": "you",
  "name": "my-vue-app",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "vue": "^2.6.10",
    "vue-gettext": "^2.1.0",
    "vuex": "^3.1.1"
  },
  "devDependencies": {},
  "config": {
    "bin": "../../../../node_modules/.bin" // This should point to the node_modules/.bin folder in tuleap/ root folder
  },
  "scripts": {
    "build": "NODE_ENV=production $npm_package_config_bin/webpack --mode=production",
    "watch": "NODE_ENV=watch $npm_package_config_bin/concurrently --raw --kill-others '$npm_package_config_bin/webpack --watch --mode=development' 'CI=true npm test -- --watch'",
    "test": "$npm_package_config_bin/jest"
  }
}

Note

All the development dependencies are available at the tuleap root, hence the config.bin.

Use the npm scripts to build up the application or to launch the unit tests.

npm run build    # For a production build, outputs minified code.
npm run watch    # Run the :ref:`Jest <jest_unit_test>` unit tests and build the app in watch mode at the same time.
npm run test     # Run the :ref:`Jest <jest_unit_test>` unit tests only once.

Once you have a package.json file, you will also need a tsconfig.json file to configure Typescript.

// tuleap/plugins/my-plugin/scripts/tsconfig.json
{
    "extends": "../../../tsconfig.json",
    "compilerOptions": {
        "module": "es2015" //redefine otherwise vue-jest crashes: https://github.com/vuejs/vue-jest/issues/144
    },
    "include": [
        "my-vue-app/**/*"
    ],
    "exclude":  [
        "node_modules",
        "my-vue-app/**/*.test.ts"
    ]
}

Bootstrap your application

This section will explain you how to properly integrate your application in Tuleap.

Create a mount point

To allow your app to run in Tuleap, you may need to create a mount point in a mustache template. Your mount point needs to have a unique identifier in order to be easily retrieved from the DOM. This is also the place where you can pass some data from PHP to JavaScript via data-* attributes:

<div class="tlp-pane">
    <div id="my-vue-app-mount-point"
        data-user="{{ user }}"
    ></div>
</div>

Once your mount point is ready, head to your index.ts file.

// tuleap/plugins/my-plugin/scripts/<my_vue_app>/src/index.ts

import Vue      from 'vue';
import MyVueApp from './components/MyVueApp.vue';

document.addEventListener('DOMContentLoaded', () => {
    // Retrieve the mount point from the DOM
    const vue_mount_point_id = "my-vue-app-mount-point";
    const vue_mount_point = document.getElementById(vue_mount_point_id);

    if (!vue_mount_point) {
        throw new Error(`Could not find Vue mount point ${vue_mount_point_id}`);
    }
    const MyVueAppComponent = Vue.extend(MyVueApp);

    new MyVueAppComponent({                                     // Create a new component
        propsData: {
            user: JSON.parse(vue_mount_point.dataset.user)      // Pass the data bound to the mount point to the app
        }
    }).$mount(vue_mount_point);                                 // Mount the app on the moint point
});

Vue and Typescript

The reference language to use with Vue.js is now Typescript.

Best-practices for Tuleap

When you submit a patch for review, we may request changes to better match the following best practices. Please try to follow them. Many rules are already enforced by the pre-commit hook that runs eslint with eslint-plugin-vue.

  • Please avoid the usage of vue directives shorthands. Shorthands are nice to use but it is not obvious for the others to figure out which directive you are actually using.

  • Always use PascalCase for component names.

  • Always use multi-word names for components, for example: “DocumentSearch”. In templates, this translates as <document-search/>. See the dedicated Vue Style Guide rule.

  • Always use snake_case for computed properties. I know, there are parentheses when we define them, but they really are properties, not methods. See Tuleap coding standards.

  • Always use snake_case for props. They follow the same rule as variables.

  • Always use camelCase for methods.

  • Always use snake_case for Vuex State properties and Getters. They are properties too.

  • Always use camelCase for Vuex Mutations and Actions. They are methods.

  • Always name files and folders inside components/ with PascalCase (just like component names).

  • Always name javascript files (in all other folders) with dash-case.

  • Avoid having too many components that depend on this.$route. Inject what you need via props instead.

  • Always use named exports in Vuex Getters, Mutations and Actions. Default export may be used for State definition. Named exports make it easier to import only what we want.

  • Always use the inline export syntax export function myAction() or export const myMutation() => {}. It makes it easy to add “private” (non-exported) functions that will be reused.