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

Widget Development

Create custom widgets to extend V-Portal’s UI capabilities.

Widget Basics

Widgets are Vue.js components with V-Portal integration:

  • Access to data fields
  • Module API integration
  • Configurable properties
  • Responsive design

Widget Structure

import { WidgetBase } from 'sdk/widgets/widgetBase'; export class MyCustomWidget extends WidgetBase { // Widget metadata name = 'My Custom Widget'; description = 'A custom widget for displaying data'; icon = 'icon-name'; // Default configuration defaultConfig = { title: 'My Widget', updateInterval: 1000, showHeader: true, }; // Vue component render() { return { template: ` <div class="my-widget"> <h3>{{ title }}</h3> <div>{{ data }}</div> </div> `, data() { return { title: this.config.title, data: null, }; }, mounted() { this.startUpdates(); }, methods: { startUpdates() { // Widget logic }, }, }; } }

Data Field Integration

Subscribing to Data

import { useDataField } from 'sdk/composables/dataFields'; export default { setup() { const { value, subscribe } = useDataField('robot.status'); subscribe(newValue => { console.log('Status updated:', newValue); }); return { status: value }; }, };

Publishing Data

import { publishDataField } from 'sdk/widgets/widgetUtils' methods: { updateValue(newValue) { publishDataField('widget.output', newValue) } }

Widget Configuration

Configurable Properties

export class ConfigurableWidget extends WidgetBase { configSchema = { title: { type: 'string', label: 'Widget Title', default: 'My Widget', }, dataSource: { type: 'datafield', label: 'Data Source', required: true, }, refreshRate: { type: 'number', label: 'Refresh Rate (ms)', default: 1000, min: 100, max: 10000, }, color: { type: 'color', label: 'Color', default: '#3b82f6', }, }; }

Accessing Configuration

computed: { widgetTitle() { return this.config.title || 'Default Title' }, dataFieldName() { return this.config.dataSource } }

Styling Widgets

Using TailwindCSS

<template> <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4"> <h3 class="text-lg font-semibold mb-2">{{ title }}</h3> <div class="text-gray-600 dark:text-gray-300"> {{ content }} </div> </div> </template>

Custom Styles

render() { return { template: `<div class="my-widget">...</div>`, style: ` .my-widget { background: var(--widget-bg); border: 1px solid var(--widget-border); } ` } }

Advanced Features

API Integration

import { useModuleApi } from 'sdk/composables/moduleApi'; export default { setup() { const api = useModuleApi('robot-module'); const sendCommand = async cmd => { const result = await api.post('/command', { cmd }); return result; }; return { sendCommand }; }, };

WebSocket Support

import { useWebSocket } from 'sdk/composables/webSocket'; export default { setup() { const { connect, send, onMessage } = useWebSocket(); connect('/ws/robot'); onMessage(data => { console.log('Received:', data); }); return { send }; }, };

Widget Lifecycle

export default { mounted() { // Widget added to page this.initialize(); }, beforeUnmount() { // Widget removed from page this.cleanup(); }, methods: { initialize() { // Start subscriptions // Initialize connections }, cleanup() { // Clean up subscriptions // Close connections }, }, };

Registering Widgets

In a Module

import { MyCustomWidget } from './widgets/MyCustomWidget'; export class MyModule extends ModuleBase { registerWidgets() { this.addWidget(new MyCustomWidget()); } }

Standalone Widget

import { registerWidget } from 'sdk/widgets/widgetUtils'; import { MyWidget } from './MyWidget'; registerWidget(new MyWidget());

Best Practices

  • Responsive Design: Ensure widgets work at different sizes
  • Performance: Minimize re-renders and DOM updates
  • Cleanup: Always unsubscribe and cleanup resources
  • Error Handling: Handle API failures gracefully
  • Accessibility: Add proper ARIA labels and keyboard support

Example: Status Widget

import { WidgetBase } from 'sdk/widgets/widgetBase'; import { useDataField } from 'sdk/composables/dataFields'; export class StatusWidget extends WidgetBase { name = 'Status Display'; render() { return { template: ` <div class="status-widget" :class="statusClass"> <div class="status-icon">â—Ź</div> <div class="status-text">{{ statusText }}</div> </div> `, setup() { const { value: status } = useDataField('robot.status'); return { status }; }, computed: { statusClass() { return `status-${this.status?.toLowerCase() || 'unknown'}`; }, statusText() { return this.status || 'Unknown'; }, }, }; } }

Next Steps

Last updated on