How-To: Creating and Registering Blade Widgets with useWidgets
¶
This guide provides a practical walkthrough on how to create custom widget components and register them for display within specific blades in your VC-Shell application using the useWidgets
composable.
Prerequisites¶
- Understanding of Vue 3 Composition API (
ref
,computed
,defineExpose
). - Familiarity with the
useWidgets
API Reference. - Knowledge of the
VcWidget
atomic component, recommended as a base for blade widgets. - Basic understanding of VC-Shell's blade system.
Core Concepts¶
- Blade Widgets: Self-contained Vue components designed to offer supplementary information or interactive elements within a VC-Shell blade.
useWidgets
Composable: The primary API for managing the registration, state, and lifecycle of these blade widgets.VcWidget
Component: A standardized UI component provided by VC-Shell, ideal for building custom blade widgets with a consistent look and feel.
Step 1: Creating Your Blade Widget Component¶
When building a new widget for a blade, using VcWidget
as its foundation is highly recommended. This ensures visual consistency with the VC-Shell framework and provides common widget functionalities.
Example: ProductStockWidget.vue
This widget displays stock information for a product and could be used within a product details blade.
// ProductStockWidget.vue
<template>
<VcWidget
:title="widgetTitle"
:value="stockLevel"
:icon="stockIcon"
@click="handleWidgetClick"
/>
</template>
<script setup lang="ts">
import { VcWidget } from '@vc-shell/framework'; // Adjust import path if necessary
import { ref, computed, defineProps, defineEmits, defineExpose, onMounted, watch } from 'vue';
const props = defineProps<{
productId: string;
initialStock?: number;
modelValue?: any; // For two-way binding
}>();
const emit = defineEmits<{
'update:modelValue': [value: any];
'stock-updated': [stock: number];
}>();
const stockLevel = ref(props.initialStock || 0);
const widgetTitle = computed(() => `Stock: ${props.productId}`);
const stockIcon = computed(() => stockLevel.value > 0 ? 'material-inventory_2' : 'material-production_quantity_limits');
// Watch for stock changes and emit custom events
watch(stockLevel, (newStock) => {
emit('stock-updated', newStock);
// Update model value with stock information
emit('update:modelValue', {
productId: props.productId,
stock: newStock,
timestamp: Date.now()
});
});
async function fetchStock() {
// Simulate API call to fetch stock
await new Promise(resolve => setTimeout(resolve, 500));
const newStock = Math.floor(Math.random() * 100);
stockLevel.value = newStock;
console.log(`Stock for ${props.productId} updated to: ${stockLevel.value}`);
}
function handleWidgetClick() {
console.log(`ProductStockWidget for ${props.productId} clicked.`);
// Maybe open a stock history blade or refresh stock
fetchStock();
}
onMounted(() => {
fetchStock(); // Initial stock fetch
});
// Expose a function to be called via updateActiveWidget
defineExpose({
refreshStock: fetchStock
});
</script>
Step 2: Registering the Widget for a Specific Blade¶
Widgets are registered using the useWidgets
composable, typically within the setup
function of the component that defines or manages the blade.
// In your blade component (e.g., ProductDetailsBlade.vue)
import { useWidgets, type IWidget } from '@vc-shell/framework';
import { onMounted, onUnmounted, markRaw, ref, computed } from 'vue';
import ProductStockWidget from './ProductStockWidget.vue'; // Path to your widget
import { BladeInstance } from '@vc-shell/framework';
const currentBlade = inject<IBladeInstance>(BladeInstance); // Inject the current blade instance
const currentProductId = ref('PROD123'); // Product ID for context
const { registerWidget, unregisterWidget } = useWidgets();
onMounted(() => {
const stockWidgetConfig: IWidget = {
id: `stock-widget-${currentProductId.value}`, // Unique ID for this widget instance
component: markRaw(ProductStockWidget), // Your widget component
props: {
productId: currentProductId.value,
initialStock: 10, // Example initial prop
},
events: {
"update:modelValue": (val: unknown) => {
// Handle model value updates from the widget
console.log("Widget model updated:", val);
},
"stock-updated": (newStock: number) => {
// Handle custom events emitted by the widget
console.log("Stock updated to:", newStock);
}
},
isVisible: computed(() => currentProductId.value !== 'PROD_ARCHIVED'), // Conditional visibility
updateFunctionName: 'refreshStock' // Matches the function exposed by ProductStockWidget
};
registerWidget(stockWidgetConfig, currentBlade.value.id);
});
onUnmounted(() => {
unregisterWidget(`stock-widget-${currentProductId.value}`, currentBlade.value.id);
});
id
: Must be unique for each widget instance within a blade.component
: UsemarkRaw()
for performance.props
: Pass any necessary reactive or static data to your widget component.events
: Define event handlers for events emitted by the widget (e.g.,update:modelValue
, custom events).bladeId
: Specifies which blade the widget belongs to.updateFunctionName
: Allows the widget to be updated viaupdateActiveWidget()
.
Step 3: Handling Widget Events¶
Widgets can emit events that can be handled by their parent components. This is particularly useful for two-way data binding and custom widget interactions.
Setting up Event Handlers:
When registering a widget, you can define event handlers in the events
property:
// Example: Widget with blade context binding
const bladeContext = ref<DetailsBladeContext>({
scope: { canEdit: true },
data: null
});
const contextAwareWidgetConfig: IWidget = {
id: 'context-aware-widget',
component: markRaw(MyContextWidget),
props: {
modelValue: bladeContext, // Pass reactive context
},
events: {
// Handle two-way binding with modelValue
"update:modelValue": (val: unknown) => {
bladeContext.value = val as DetailsBladeContext;
console.log("Blade context updated:", val);
},
// Handle custom widget events
"widget-action": (actionType: string, payload: any) => {
console.log(`Widget action: ${actionType}`, payload);
// Handle widget-specific actions
}
},
isVisible: computed(() => bladeContext.value.scope?.canEdit),
updateFunctionName: "refresh"
};
Real-world Example: Product Details Widget with Context Binding
// In ProductDetailsBlade.vue
const productContext = ref({
productId: 'PROD123',
isEditable: true,
lastModified: null
});
const productWidgetConfig: IWidget = {
id: 'product-details-widget',
component: markRaw(ProductDetailsWidget),
props: {
modelValue: productContext,
},
events: {
"update:modelValue": (val: unknown) => {
productContext.value = val as typeof productContext.value;
// Auto-save changes or trigger validation
saveProductContext(productContext.value);
},
"product-saved": (productData: any) => {
// Handle successful product save
showSuccessNotification('Product saved successfully');
},
"validation-error": (errors: string[]) => {
// Handle validation errors
showErrorNotification(errors.join(', '));
}
},
isVisible: computed(() => productContext.value.isEditable)
};
Step 4: Updating Widget State or Props¶
Updating Widget Props:
Use updateWidget
to change a widget's properties like isVisible
or props
after registration.
const { updateWidget } = useWidgets();
function toggleWidgetVisibility(widgetId: string, isVisible: boolean) {
updateWidget({
id: widgetId,
bladeId: currentBladeId.value, // Ensure this is the correct blade ID
widget: { isVisible },
});
}
// If currentProductId changes, the 'productId' prop of ProductStockWidget will update automatically
// because it was passed as `currentProductId.value` (a reactive ref) during registration.
// For props not initially bound to a reactive source, or to change other IWidget fields:
function updateWidgetDetails(widgetId: string, newProps: Partial<IWidget['props']>) {
updateWidget({
id: widgetId,
bladeId: currentBladeId.value,
widget: { props: newProps } // Can also update .title, .isVisible etc.
});
}
Triggering a Widget's Exposed Function:
If a widget, like ProductStockWidget
, exposes a function via defineExpose
and its updateFunctionName
was set during registration, you can trigger this function on the "active" widget. VcWidget
often handles setting itself as active on click.
const { updateActiveWidget, isActiveWidget, setActiveWidget } = useWidgets();
// Example: Force a stock refresh on the active widget
function refreshActiveWidgetStock() {
// VcWidget typically calls setActiveWidget on click.
// If not using VcWidget or needing programmatic activation, you might need to call setActiveWidget first.
// e.g., if you have the widget's exposed instance:
// setActiveWidget({ exposed: productStockWidgetInstance.exposed, widgetId: `stock-widget-${currentProductId.value}` });
// Then call the exposed function (e.g., 'refreshStock')
// This assumes `stock-widget-${currentProductId.value}` is currently the active widget.
if (isActiveWidget(`stock-widget-${currentProductId.value}`)) {
updateActiveWidget(); // This will attempt to call 'refreshStock' on the active widget
} else {
console.warn("Stock widget is not active. Cannot refresh.");
}
}
Step 5: Unregistering Blade Widgets¶
Always unregister widgets when the component that registered them (usually the blade itself) is unmounted.
// (Already shown in Step 2)
import { onUnmounted } from 'vue';
// ...
onUnmounted(() => {
unregisterWidget(`stock-widget-${currentProductId.value}`, currentBladeId.value);
// To remove all widgets for the current blade:
// clearBladeWidgets(currentBladeId.value);
});
Global Pre-registration for Core Widgets¶
For widgets that are part of the core framework or foundational modules and need to be available very early, VC-Shell offers a global registerWidget
function (distinct from the one returned by useWidgets()
).
// In a module's main setup file (e.g., my-module/index.ts)
import { registerWidget as globalRegisterWidget, type IWidget } from '@vc-shell/framework';
import { markRaw } from 'vue';
import MySystemStatusWidget from './internal/MySystemStatusWidget.vue';
const systemWidget: IWidget = {
id: 'system-status-main',
component: markRaw(MySystemStatusWidget),
props: { updateInterval: 60000 },
};
// Register for a known blade ID, e.g., a main dashboard blade
globalRegisterWidget(systemWidget, 'app-dashboard-main');
Best Practices¶
- Unique IDs: Ensure
IWidget.id
is unique for each widget instance on a blade. markRaw
: UsemarkRaw
forIWidget.component
for better performance.- Cleanup: Always use
unregisterWidget
orclearBladeWidgets
inonUnmounted
. - Reactivity: Pass reactive data to widgets via their
props
for dynamic updates. - Event Handling: Use the
events
property to handle widget events, especially for two-way data binding withupdate:modelValue
. VcWidget
Base: UtilizeVcWidget
as the base for blade widgets for consistency.updateFunctionName
: Define for widgets that need external refresh/update triggers.- Blade Context: Manage
bladeId
carefully for correct widget association.
This guide equips you to build and manage dynamic, modular widgets within your VC-Shell application's blades, enhancing user experience and information display.