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
-
Make sure your current working directory in a command-line ends in
your-module/frontend, for example:C:\coding\module\frontend -
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' -
Within the newly created
package.jsonfile in the frontend folder, you’ll need to add the following:"scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, -
Remove the following line from the
package.jsonfile:"main": "index.js", -
Now use
npm installto 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.vuefile - The
myWidgetInterface.tsfile - The definition in the
module.tsfile
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 totrue, your widget will have no background and no border, making it completely styled by you.takeControl: boolean— If set totrue, 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;