🎉 V-Portal 0.5.2 is released! Read more
Skip to Content

Frontend Setup Guide

This is an entire setup guide for doing frontend module development for V-Portal.

Installation & Setup

This is the required file structure for frontend modules to build correctly.

Frontend File Structure

    • vite.config.js
    • package.json
    • package-lock.json
      • main.js
      • module.ts
        • your
        • file
        • structure

Initialise Frontend

  1. Make sure your current working directory in a command-line ends in your-module/frontend, for example: C:\coding\module\frontend

  2. Run npm init

    > npm init > package name: (frontend) new-module > version: (1.0.0) > description: > entry point: (index.js) > test command: > git repository: > keywords: > author: Vikaso Dev > license: (ISC) > type: (commonjs) module <-- Make sure to make the type 'module'
  3. Within the newly created package.json file in the frontend folder, you’ll need to add the following:

    "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
  4. Remove the following line from the package.json file:

    "main": "index.js",
  5. Now use npm install to install the following packages:

    > npm install vite > npm install vite-plugin-css-injected-by-js > npm install @vitejs/plugin-vue > npm install vue > npm install v-portal-components > npm install axios

Main.js

Simply copy-paste the following into the main.js file:

import module from './module'; import data from '../../module.json'; export default module;

Vite Config

This is the required vite config for the build to correctly package your module.

Firstly, create vite.config.js in your frontend directory, then copy-paste the following:

import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; import path from 'node:path'; export default defineConfig({ plugins: [vue(), cssInjectedByJsPlugin()], build: { lib: { entry: path.resolve(__dirname, 'src/main.js'), name: 'your-module', // TODO: Change to your module name formats: ['es'], fileName: () => 'main.js', }, target: 'esnext', minify: true, cssCodeSplit: false, // keep styles together rollupOptions: { external: ['vue'], output: { inlineDynamicImports: true, // single file globals: { vue: 'Vue', }, }, }, }, define: { 'process.env': {}, 'process.browser': true, }, });

Module.ts

Within your module.ts file, there will be the exported module base, which defines the frontend additions to V-Portal from your module.

// You can import your module data from the module.json into there import data from '../../module.json'; // markRaw is used in Vue to render components without errors import { markRaw } from 'vue'; // this is the type-def for the exported object, import from v-portal-components import { moduleBase } from '../node_modules/v-portal-components/dist/modules/moduleBase'; export default { name: data.name, moduleID: data.id, version: data.version, author: data.author, description: data.description, pages: [ { title: 'Tools Page', group: 'tools', path: 'tools-module-page', component: markRaw(ToolsPage), // ToolsPage would be a .vue file icon: ``, }, { title: 'Main Page', group: 'pages', path: 'main', component: markRaw(MainPage), // MainPage would be a .vue file icon: `<svg xmlns="http://www.w3.org/2000/svg" width="800px" height="800px" class="w-4 h-4"> </svg>`, // This is a valid SVG icon that will render before the item on the navbar }, ], widgets: [ widgetInterfaceFile, // These are not .vue files, these are defined interfaces for widgets that CounterWidgetInterface, // get loaded onto v-portal when the module is loaded VideoWidgetInterface, // These are shown in the Widgets section ], onLoad: (app: App) => {}, // A function called on module load to allow your module to load vue plugins // with the current Vue app instance passed in } as moduleBase;

Module Registry

When developing for V-Portal in Vue, there is a Vue native function called inject which allows us to provide an object with the key moduleRegistry, then you can get the object via an inject.

Example

import { inject } from 'vue'; const moduleRegistry = inject('moduleRegistry');

Module Registry Interface (ts)

export interface mdReg { dataFields: { value: Map<number, DataFieldV2> }; // Read Only setPageTopNavData: ( baseURL: string, slotComponent: Component, slotProps: any ) => void; // Function getAPIClient: (id: string) => AxiosInstance; // Function getStreamAPIClient: (id: string) => AxiosInstance; getModuleWebSocketURL: (id: string) => string; userData: () => { description: string; id: number; name: string; type: number; }[]; // Function, readonly } await (moduleRegistry.userData)()[0];

DataFields

This is a ref copy of the current frontend Data Fields Map, storing the current loaded data fields, values, IDs and names.

setPageTopNavData

This function allows you to set a custom component in the slot on the top nav bar, as well as pass callbacks to it via slotProps which are accessed via the props interface in Vue.

getAPIClient

This function returns a valid AxiosInstance set up to call directly to the module backend routes. For example, if your backend has a /getData route:

import * as module from 'module.json' // getting the API client function from the moduleRegistry const { getAPIClient } = inject("moduleRegistry"); // calling with the moduleID to get their APIClient const APIClient: AxiosInstance = getAPIClient(module.moduleID) const response = await APIClient.get('/getData');

getStreamAPIClient

Returns a valid AxiosInstance set up to call directly to the module backend streaming routes. Functions exactly the same as getAPIClient but with a different base URL.

getModuleWebSocketURL

Returns a string which is the valid WebSocket URL for your module to connect to a module backend defined WebSocket.

Widgets

The widget system consists of:

  • The myWidget.vue file
  • The myWidgetInterface.ts file
  • The definition in the module.ts file

A widget should handle its own:

  • Editing State
  • Disabled State
  • Render State

When creating your widget, we recommend using this template:

<template> <div class="flex flex-col w-full h-full"> <div v-if="!isEditing"> {/* Render State */} {/* This is the element shown on the actual widget */} </div> <div v-if="isEditing"> {/* Editing State */} {/* This section is only rendered in a modal when the user selects 'edit' for your widget. It is limited to 95% screen-width and 90% screen-height. This section should include a form or input for the user to change the widget config, then using setComData and a handler function you would update the component data so it's persistent. */} </div> </div> </template> <script setup lang="ts"> interface yourComponentData { some: string; component: string; data: string; types: string; } interface widgetProps { isEditing: boolean; componentData: yourComponentData; setComData: (componentData: yourComponentData) => {}; isDisabled: boolean; } const props = defineProps<widgetProps>(); /** * Using the 'setComData' function is how you have persistent data for your widget. * Any data stored in the componentData will be stored in the page data. */ // Call this onChange from your form for the editing state const handleNewData = (newData) => { // This way you never lose old data by accident const newComData = { ...OldComData, ...newData }; props.setComData(newComData); } </script>

Props

The following props are passed to all loaded widgets:

interface widgetProps { isEditing: boolean; componentData: any; setComData: (componentData: any) => {}; isDisabled: boolean; }

Component Data

The component data is used as your store to keep data between loads.

Default Editable Component Data Keys

Some keys can be used to define settings for a widget:

  • borderless: boolean — If set to true, your widget will have no background and no border, making it completely styled by you.
  • takeControl: boolean — If set to true, your widget will require Take Control to be active. When the user is not in control, a black box will cover the entire widget disabling any UI interaction.

Disallowed Component Data Keys

The following keys cannot be used in your componentData:

  • JOSH: string — Used internally for storing the state-based expression used for enabling/disabling the widget.

Widget Interface

The second requirement for making your widget registerable in V-Portal is defining the widget interface.

// Imports import { markRaw } from 'vue'; import yourWidget from './yourWidget.vue'; // default export required for importing the widgetInterface into the module.ts export default { id: 'moduleName.WidgetName', name: 'Widget Name', description: 'Description of your new widget to show on the available widgets table', category: 'module', // this can be 'module' or 'utility' icon: '🖱️', // string icon component: markRaw(yourWidget), // Make sure you use markRaw here else it will not render defaultSize: { w: 4, h: 4 }, // Default page grid size, minimum width is 4 componentData: { borderless: true, takeControl: true, your: "string", default: { a: true, b: "string" }, widget: false, config: ":)" }, setComData: () => {}, moduleId: '', // Can be left empty, overwritten on load by V-Portal };

Registering the Widget in module.ts

In your module.ts file, the widgets array in the default export holds all valid widgetInterface objects. Simply import your widget interface file and append it to the array:

import yourWidgetInterface from './widgets/newWidgetInterface.ts' export default { ..., widgets: [ yourWidgetInterface ] } as moduleBase;
Last updated on