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