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 should proceed to build up you application.

Before beginning

Vue.js can be used along with some additional libraries like Pinia 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 + Pinia

Has several pages to display

Vue.js + Pinia + 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 translations
  |-- scripts/
       |-- my-vue-app/
            |-- frontend-assets/        # Generated by Vite. Never edit manually.
            |-- po/                     # Localization strings
                 |-- fr_FR.po           # Localized strings for French
            |-- src/                    # The app source-code
                 |-- api/               # REST API consumers
                 |-- components/        # Vue components
                      |-- MyFeature/
                      |-- OtherFeature/
                 |-- store/             # Pinia store modules (actions/mutations/getters)
                 |-- router/            # Vue-router modules
                 |-- main.ts            # App bootstrapping
            |-- jest.config.js          # Unit tests bootstrapping
            |-- package.json            # Declares the App, its dependencies and its build script.
            |-- pnpm-lock.yaml          # Generated by pnpm. Never edit manually.
            |-- tsconfig.json           # Typescript configuration
            |-- vite.config.ts          # Vite configuration to build the App

Build your Vue application

The build system will read build-manifest.json to understand how and where it needs to extract translated strings.

// tuleap/plugins/my-plugin/build-manifest.json
{
    "name": "my-plugin",
    "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 create a vite.config.ts file. This file should be located in my-plugin/scripts/my-vue-app/.

// tuleap/plugins/my-plugin/scripts/my-vue-app/vite.config.ts
import { vite } from "@tuleap/build-system-configurator";
import * as path from "path";
import PoGettextPlugin from "@tuleap/po-gettext-plugin";

export default vite.defineAppConfig(
    {
        plugin_name: path.basename(path.resolve(__dirname, "../..")),
        sub_app_name: path.basename(__dirname),
    },
    {
        plugins: [PoGettextPlugin.vite()],
        build: {
            rollupOptions: {
                input: {
                    "my-vue-app": path.resolve(__dirname, "src/main.ts"),
                },
            },
        },
    }
);

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

// tuleap/plugins/my-plugin/scripts/my-vue-app/package.json
{
  "author": "Enalean Team",             // or yourself
  "name": "@tuleap/plugin-my-plugin-my-vue-app",
  "homepage": "https://tuleap.org",     // or your plugin's homepage
  "license": "GPL-2.0-or-later",        // or your license
  "private": true,
  "type": "module",
  "dependencies": {
    "@tuleap/dom": "workspace:*",
    "pinia": "^2.0.21",
    "vue": "^3.2.37",
    "vue3-gettext": "^2.2.1"
  },
  "devDependencies": {
    "@tuleap/build-system-configurator": "workspace:*",
    "@tuleap/po-gettext-plugin": "workspace:*",
  },
  "scripts": {
    "typecheck": "vue-tsc --noEmit"
    "build": "vite build",
    "watch": "vite build --watch --mode development --minify false",
    "test": "jest"
  }
}

Note

All the Vite and Jest dependencies are available at the tuleap root folder.

Use the pnpm scripts to build up the application or to run the unit tests.

pnpm run typecheck # Run TypeScript type check on your code and unit tests.
pnpm run build     # For a production build, outputs minified code.
pnpm run watch     # Build the app in watch mode.
pnpm test          # Run the Jest 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/my-vue-app/tsconfig.json
{
    "extends": "../../../../tsconfig.json",
    "compilerOptions": {
        "module": "es2020",
        "types": ["jest"], // Add global types needed by your application
    },
    "include": ["src/**/*"]
}

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 TypeScript via data-* attributes:

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

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

// tuleap/plugins/my-plugin/scripts/my-vue-app/src/main.ts

import { createApp } from "vue";
import { getPOFileFromLocaleWithoutExtension, initVueGettext } from "@tuleap/vue3-gettext-init";
import { createGettext } from "vue3-gettext";
import { getDatasetItemOrThrow, selectOrThrow } from "@tuleap/dom";
import MyVueApp from "./components/MyVueApp.vue";
import { useRootStore } from "./stores/root";

const MOUNT_POINT_SELECTOR = "#my-vue-app-mount-point";

document.addEventListener("DOMContentLoaded", async () => {
    // Retrieve the mount point from the DOM
    const mount_point = selectOrThrow(MOUNT_POINT_SELECTOR);

    const app = createApp(MyVueApp);

    // Dynamically import the translations relevant to the current user's language.
    app.use(
        await initVueGettext(
            createGettext,
            (locale: string) => import(`../po/${getPOFileFromLocaleWithoutExtension(locale)}.po`)
        )
    );

    app.use(pinia);
    const root_store = useRootStore();

    // Retrieve the JSON data from the mount point. The dataset key is in CamelCase.
    const user_info = JSON.parse(getDatasetItemOrThrow(mount_point, "userInfo"));

    // Pass the mount-point data to the pinia store
    root_store.setUserInfo(user_info);

    // Replace the mount point's HTML element with the rendered App
    app.mount(mount_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 Pinia State properties and Getters. They are properties too.

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

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

  • Always name typescript 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 Pinia 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.

  • Be careful with translations, when translate is used in a <template> extraction won’t work, that means you must extract your translations into a dedicated component.

Resources