🎉 V-Portal 0.5.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