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