UNPKG

@vpwhite/vue-rule-builder

Version:

A powerful, flexible Vue 3 component library for building complex business rules with an intuitive visual interface. Transform your business logic into interactive, maintainable rule definitions.

1,387 lines (1,106 loc) โ€ข 35.5 kB
# Vue Rule Builder A powerful, flexible Vue 3 component library for building complex business rules with an intuitive visual interface. Transform your business logic into interactive, maintainable rule definitions. ## ๐Ÿš€ Features - ๐ŸŽฏ **Visual Rule Builder**: Drag-and-drop interface for creating complex business rules - ๐Ÿ”ง **Multiple Rule Types**: Support for property rules, expressions, and reusable rule references - ๐ŸŽจ **Modern UI**: Clean, responsive design with smooth animations and dark mode support - ๐Ÿ” **Real-time Validation**: Instant feedback on rule validity with detailed error messages - ๐Ÿงช **Test Panel**: Test rules against sample data with live preview - ๐Ÿ’พ **Auto-save**: Automatic local backups with history management - ๐Ÿ”Œ **Plugin Architecture**: Easy integration with any backend API - ๐Ÿ“ฑ **Mobile Responsive**: Works seamlessly on desktop, tablet, and mobile devices - ๐ŸŒ **TypeScript**: Full TypeScript support with comprehensive type definitions - ๐ŸŽ›๏ธ **Highly Customizable**: Extensive theming and configuration options ## ๐Ÿ“ฆ Installation ```bash npm install vue-rule-builder ``` ### Peer Dependencies ```bash npm install vue@^3.5.0 pinia@^2.3.0 # Optional: for advanced data fetching npm install @tanstack/vue-query@^5.56.0 ``` ## ๐Ÿ—๏ธ Architecture Overview The Vue Rule Builder follows a modular architecture with clear separation of concerns: ``` Vue Rule Builder Architecture โ”œโ”€โ”€ Core Components โ”‚ โ”œโ”€โ”€ RuleBuilder (Main container) โ”‚ โ”œโ”€โ”€ RuleGroup (Logical groups) โ”‚ โ”œโ”€โ”€ RuleRow (Individual rules) โ”‚ โ””โ”€โ”€ AddRuleDropdown (Rule creation) โ”œโ”€โ”€ Supporting Components โ”‚ โ”œโ”€โ”€ ExpressionRow (Custom expressions) โ”‚ โ”œโ”€โ”€ ReferenceRow (Reusable rules) โ”‚ โ”œโ”€โ”€ ValueSelector (Value input) โ”‚ โ””โ”€โ”€ PropertySelectorInput (Field selection) โ”œโ”€โ”€ State Management โ”‚ โ”œโ”€โ”€ RuleBuilderStore (Pinia store) โ”‚ โ”œโ”€โ”€ Instance Management (Multi-instance support) โ”‚ โ””โ”€โ”€ Auto-save System โ”œโ”€โ”€ Utilities โ”‚ โ”œโ”€โ”€ Preview Generation โ”‚ โ”œโ”€โ”€ Validation Engine โ”‚ โ””โ”€โ”€ Data Transformation โ””โ”€โ”€ Type System โ”œโ”€โ”€ RuleNodeDto (Core data structure) โ”œโ”€โ”€ TreeNode (Field definitions) โ””โ”€โ”€ ValidationResult (Error handling) ``` ## ๐ŸŽฏ Core Concepts ### Rule Types 1. **Property Rules**: Compare field values with operators (equals, contains, etc.) 2. **Expression Rules**: Custom JavaScript expressions for complex logic 3. **Reference Rules**: Reusable rule definitions for common patterns 4. **Group Rules**: Logical containers (AND/OR/NOT) for organizing rules ### Data Flow ``` User Interaction โ†’ Component Events โ†’ Store Updates โ†’ State Change โ†’ UI Re-render โ†“ Validation Engine โ†’ Error Display โ†’ User Feedback โ†“ Auto-save System โ†’ Local Storage โ†’ History Management ``` ## ๐Ÿš€ Quick Start ### Basic Usage ```vue <template> <div class="app"> <RuleBuilder v-model:rule="myRule" :entity-name="'User'" :data-providers="dataProviders" @validation-change="handleValidation" /> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { RuleBuilder, useRuleBuilderInstance } from 'vue-rule-builder' import type { RuleNodeDto, DataProviders } from 'vue-rule-builder' // Initialize rule state const myRule = ref<RuleNodeDto>({ id: 'root', kind: 0, // NodeKind.Group combinator: 0, // CombinatorKind.And not: false, children: [] }) // Data providers for API integration const dataProviders: DataProviders = { fetchBaseConfig: async ({ entityName }) => { const response = await fetch(`/api/${entityName}/config`) return response.json() }, fetchTree: async ({ entityName, path }) => { const url = path ? `/api/${entityName}/tree?path=${encodeURIComponent(path)}` : `/api/${entityName}/tree` const response = await fetch(url) return response.json() }, testRule: async (rule, testData) => { const response = await fetch('/api/rules/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rule, testData }) }) return response.json() } } const handleValidation = (result: { valid: boolean; errors: string[] }) => { console.log('Validation:', result) } </script> ``` ### With Static Data ```vue <template> <RuleBuilder :rule="myRule" :tree-data="staticTreeData" :available-references="ruleReferences" @update:rule="handleRuleChange" /> </template> <script setup lang="ts"> import { ref } from 'vue' import { RuleBuilder } from 'vue-rule-builder' import type { RuleNodeDto, TreeNode } from 'vue-rule-builder' const myRule = ref<RuleNodeDto>({ id: 'root', kind: 0, combinator: 0, not: false, children: [] }) const staticTreeData: TreeNode[] = [ { id: 'user', name: 'User', type: 'object', path: 'user', isExpandable: true, icon: 'User', children: [ { id: 'user.email', name: 'Email Address', type: 'string', path: 'user.email', isExpandable: false, icon: 'Mail' }, { id: 'user.age', name: 'Age', type: 'number', path: 'user.age', isExpandable: false, icon: 'Hash' } ] } ] const ruleReferences = [ { id: 'ref1', name: 'Active Users', description: 'Users with status = active' }, { id: 'ref2', name: 'VIP Customers', description: 'Customers with tier = VIP' } ] </script> ``` ## ๐Ÿ“‹ Complete Component API Reference ### RuleBuilder (Main Component) The primary component that orchestrates the entire rule building experience. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `rule` | `RuleNodeDto` | `undefined` | Initial rule data structure | | `instanceId` | `string` | Auto-generated UUID | Unique identifier for this rule builder instance | | `entityName` | `string` | `undefined` | Entity name for API calls and context | | `viewName` | `string` | `undefined` | View name for API calls | | `parentEntityName` | `string` | `undefined` | Parent entity name for nested contexts | | `availableTabs` | `string[]` | `['properties', 'functions', 'expression']` | Available tabs in property selector modal | | `dataProviders` | `DataProviders` | `undefined` | API providers for data fetching | | `treeData` | `TreeNode[]` | `undefined` | Static tree data for field selection | | `availableReferences` | `Reference[]` | `[]` | Available rule references for reuse | | `features` | `FeatureFlags` | `{}` | Feature toggles for enabling/disabling functionality | | `includeMeta` | `boolean` | `false` | Include metadata in v-model output | #### Events | Event | Payload | Description | |-------|---------|-------------| | `update:modelValue` | `RuleNodeDto \| { tree: RuleNodeDto, meta: RuleMeta }` | Fired when rule changes | | `update:rule` | `RuleNodeDto` | Fired when rule changes (alias for update:modelValue) | | `validation-change` | `ValidationResult` | Fired when validation state changes | | `error` | `Error` | Fired when an error occurs during operation | | `openDeveloperTools` | `string` | Fired when developer tools are opened (groupId) | #### Slots | Slot | Props | Description | |------|-------|-------------| | `default` | None | Main content area (rarely used) | | `toolbar` | `{ rule: RuleNodeDto, instanceId: string }` | Custom toolbar content | | `footer` | `{ rule: RuleNodeDto, validation: ValidationResult }` | Custom footer content | #### Example Usage ```vue <template> <RuleBuilder v-model:rule="myRule" :instance-id="'my-rule-builder'" :entity-name="'Product'" :data-providers="providers" :features="{ enableRules: true, enableExpressions: true, enableReferences: true, enableNotOperator: true, enableDeveloperTools: true }" @validation-change="handleValidation" @error="handleError" > <template #toolbar="{ rule, instanceId }"> <div class="custom-toolbar"> <button @click="exportRule">Export</button> <button @click="importRule">Import</button> </div> </template> </RuleBuilder> </template> ``` ### RuleGroup Component Manages logical groups of rules with AND/OR/NOT operators. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `group` | `RuleNodeDto` | Required | Group data structure | | `isRoot` | `boolean` | `false` | Whether this is the root group | | `parentId` | `string` | `undefined` | Parent group ID for nested groups | | `availableReferences` | `Reference[]` | `[]` | Available rule references | #### Events | Event | Payload | Description | |-------|---------|-------------| | `openDeveloperTools` | `string` | Fired when developer tools are opened | #### Internal State - `operatorValue`: Current logical operator (AND/OR) - `features`: Available features from store - `expandedNodes`: Set of expanded node IDs #### Example Usage ```vue <template> <RuleGroup :group="groupData" :is-root="true" :available-references="references" @openDeveloperTools="handleOpenDevTools" /> </template> ``` ### RuleRow Component Individual rule component for property-based rules. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `rule` | `RuleNodeDto` | Required | Rule data structure | | `groupId` | `string` | Required | Parent group ID | | `entityName` | `string` | `undefined` | Entity name for API calls | | `treeData` | `TreeNode[]` | `undefined` | Static tree data | | `treeUrl` | `string` | `undefined` | Tree data API URL | | `functionsUrl` | `string` | `undefined` | Functions API URL | #### Events | Event | Payload | Description | |-------|---------|-------------| | `remove` | `void` | Fired when rule is removed | #### Internal State - `selectedProperty`: Currently selected field - `selectedFunction`: Currently selected function - `operatorValue`: Current comparison operator - `valuesValue`: Current rule values - `isEditing`: Whether in edit mode - `showPropertySelector`: Whether property selector is open - `parametersExpanded`: Whether function parameters are expanded #### Example Usage ```vue <template> <RuleRow :rule="ruleData" :group-id="'group-1'" :entity-name="'User'" @remove="handleRemove" /> </template> ``` ### ExpressionRow Component Custom expression rule component for JavaScript expressions. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `rule` | `RuleNodeDto` | Required | Rule data structure | | `groupId` | `string` | Required | Parent group ID | #### Events | Event | Payload | Description | |-------|---------|-------------| | `remove` | `void` | Fired when rule is removed | #### Internal State - `isEditing`: Whether in edit mode - `draftExpression`: Current expression being edited - `isValid`: Whether current expression is valid #### Example Usage ```vue <template> <ExpressionRow :rule="expressionRule" :group-id="'group-1'" @remove="handleRemove" /> </template> ``` ### ReferenceRow Component Reusable rule reference component. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `rule` | `RuleNodeDto` | Required | Rule data structure | | `groupId` | `string` | Required | Parent group ID | | `entityName` | `string` | `undefined` | Entity name for API calls | | `availableReferences` | `Reference[]` | `[]` | Available rule references | #### Events | Event | Payload | Description | |-------|---------|-------------| | `remove` | `void` | Fired when rule is removed | #### Internal State - `selectedReference`: Currently selected reference - `referenceOptions`: Available reference options - `isValid`: Whether selected reference is valid #### Example Usage ```vue <template> <ReferenceRow :rule="referenceRule" :group-id="'group-1'" :available-references="references" @remove="handleRemove" /> </template> ``` ### AddRuleDropdown Component Dropdown component for adding new rules. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `groupId` | `string` | Required | Parent group ID | | `size` | `'sm' \| 'default'` | `'default'` | Button size | #### Events | Event | Payload | Description | |-------|---------|-------------| | `addRule` | `[groupId: string, kind: NodeKind]` | Fired when a rule is added | #### Internal State - `isOpen`: Whether dropdown is open - `features`: Available features from store #### Example Usage ```vue <template> <AddRuleDropdown :group-id="'group-1'" size="sm" @add-rule="handleAddRule" /> </template> ``` ### ValueSelector Component Component for selecting and configuring rule values. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `source` | `ValueSource` | Required | Value source type | | `leftFieldPath` | `string` | `''` | Left field path for context | | `leftFieldType` | `string` | `'string'` | Left field type | | `multiValues` | `RuleValueType[]` | `[]` | Multiple values | | `inlineParameters` | `boolean` | `false` | Show parameters inline | | `maxItems` | `number` | `undefined` | Maximum number of items | | `requiredBadgeText` | `string` | `undefined` | Required badge text | #### Events | Event | Payload | Description | |-------|---------|-------------| | `update:source` | `ValueSource` | Fired when source changes | | `update:multiValues` | `RuleValueType[]` | Fired when values change | #### Internal State - `selectedSource`: Currently selected source - `contextualData`: Available contextual data - `loading`: Whether data is loading #### Example Usage ```vue <template> <ValueSelector :source="ValueSource.LITERAL" :left-field-path="'user.email'" :left-field-type="'string'" :multi-values="values" @update:source="handleSourceChange" @update:multi-values="handleValuesChange" /> </template> ``` ### PropertySelectorInput Component Input component for selecting properties and functions. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `modelValue` | `ParameterValueDto` | `undefined` | Current value | | `instanceId` | `string` | `undefined` | Instance ID | | `entityName` | `string` | Required | Entity name | | `viewName` | `string` | `undefined` | View name | | `parentEntityName` | `string` | `undefined` | Parent entity name | | `availableTabs` | `string[]` | `['properties', 'functions', 'expression']` | Available tabs | | `dataProviders` | `DataProviders` | `undefined` | Data providers | | `treeData` | `TreeNode[]` | `undefined` | Static tree data | | `treeUrl` | `string` | `undefined` | Tree data URL | | `treeNodesUrl` | `string` | `undefined` | Tree nodes URL | | `childNodesUrl` | `string` | `undefined` | Child nodes URL | | `functionsUrl` | `string` | `undefined` | Functions URL | | `enableExpression` | `boolean` | `false` | Enable expression mode | | `allowObjectSelection` | `boolean` | `false` | Allow object selection | | `autoFocusTree` | `boolean` | `false` | Auto focus tree | #### Events | Event | Payload | Description | |-------|---------|-------------| | `update:modelValue` | `ParameterValueDto` | Fired when value changes | | `selectProperty` | `TreeNode` | Fired when property is selected | | `selectFunction` | `FunctionCallDto` | Fired when function is selected | | `applyExpression` | `string` | Fired when expression is applied | #### Example Usage ```vue <template> <PropertySelectorInput v-model="selectedValue" :entity-name="'User'" :data-providers="providers" :enable-expression="true" @select-property="handlePropertySelect" @select-function="handleFunctionSelect" /> </template> ``` ### DeveloperConsole Component Developer tools panel for debugging and testing. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `instanceId` | `string` | Required | Instance ID | | `isVisible` | `boolean` | `false` | Whether console is visible | | `defaultHeight` | `number` | `400` | Default height in pixels | | `rule` | `RuleNodeDto` | Required | Current rule | #### Events | Event | Payload | Description | |-------|---------|-------------| | `close` | `void` | Fired when console is closed | | `resize` | `number` | Fired when console is resized | #### Internal State - `activeTab`: Currently active tab - `height`: Current height #### Example Usage ```vue <template> <DeveloperConsole :instance-id="'my-instance'" :is-visible="showConsole" :rule="currentRule" @close="handleClose" @resize="handleResize" /> </template> ``` ### ValidationPanel Component Panel for displaying validation results and errors. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `rule` | `RuleNodeDto` | Required | Rule to validate | #### Internal State - `validationResult`: Current validation result - `totalRules`: Total number of rules - `totalGroups`: Total number of groups #### Example Usage ```vue <template> <ValidationPanel :rule="currentRule" /> </template> ``` ### TestPanel Component Panel for testing rules against sample data. #### Props None (uses store instance) #### Internal State - `mode`: Test mode ('entity' or 'json') - `testData`: Test data - `isTesting`: Whether test is running - `testResult`: Test result #### Example Usage ```vue <template> <TestPanel /> </template> ``` ### JsonEditor Component JSON editor for rule data. #### Props None (uses store instance) #### Internal State - `jsonText`: JSON text content - `error`: JSON parsing error - `isValid`: Whether JSON is valid #### Example Usage ```vue <template> <JsonEditor /> </template> ``` ### AutosaveHistory Component Component for managing autosave history. #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `instanceId` | `string` | Required | Instance ID | #### Events | Event | Payload | Description | |-------|---------|-------------| | `restore` | `AutosaveEntry` | Fired when entry is restored | | `delete` | `string` | Fired when entry is deleted | #### Internal State - `entries`: Autosave entries - `loading`: Whether loading - `selectedEntry`: Currently selected entry #### Example Usage ```vue <template> <AutosaveHistory :instance-id="'my-instance'" @restore="handleRestore" @delete="handleDelete" /> </template> ``` ## ๐Ÿช Store API ### useRuleBuilderStore() The main Pinia store for managing rule builder state. ```typescript const store = useRuleBuilderStore() // Store methods store.addRule(instanceId: string, groupId: string, kind?: NodeKind) store.removeRule(instanceId: string, groupId: string, ruleId: string) store.updateRule(instanceId: string, groupId: string, ruleId: string, updates: Partial<RuleNodeDto>) store.validateRules(rule: RuleNodeDto): ValidationResult store.testRule(instanceId: string): Promise<TestResult> store.generateNaturalLanguage(rule: RuleNodeDto): string store.generateLambdaExpression(rule: RuleNodeDto): string ``` ### useRuleBuilderInstance(instanceId: string) Get a scoped instance of the rule builder store. ```typescript const instance = useRuleBuilderInstance('my-instance') // Instance methods instance.addRule(groupId: string, kind?: NodeKind) instance.removeRule(groupId: string, ruleId: string) instance.updateRule(groupId: string, ruleId: string, updates: Partial<RuleNodeDto>) instance.validateRules(rule: RuleNodeDto): ValidationResult instance.testRule(): Promise<TestResult> ``` ## ๐Ÿ”ง Data Providers ### DataProviders Interface ```typescript interface DataProviders { fetchBaseConfig?: (params: { entityName: string viewName?: string parentEntityName?: string }) => Promise<{ functions?: FunctionCallDto[] operators?: { comparison?: OperatorInfo[] group?: Array<{ value: 'AND' | 'OR'; label: string }> } availableTabs?: string[] availableProperties?: TreeNode[] }> fetchTree?: (params: { entityName: string viewName?: string parentEntityName?: string path?: string }) => Promise<TreeNode[]> testRule?: (rule: RuleNodeDto, testData: any) => Promise<any> fetchContextualData?: (fieldPath: string) => Promise<any[]> } ``` ### Complete Implementation Example #### Backend API Endpoints ```csharp // ASP.NET Core Controller [ApiController] [Route("api/rule-builder")] public class RuleBuilderController : ControllerBase { [HttpPost("config")] public async Task<IActionResult> GetBaseConfig([FromBody] ConfigRequest request) { var config = await _ruleBuilderService.GetBaseConfigAsync( request.EntityName, request.ViewName, request.ParentEntityName ); return Ok(new { functions = config.Functions, operators = new { comparison = config.Operators, group = new[] { new { value = "AND", label = "And" }, new { value = "OR", label = "Or" } } }, availableTabs = new[] { "properties", "functions", "expression" }, availableProperties = config.AvailableProperties }); } [HttpGet("tree")] public async Task<IActionResult> GetTree([FromQuery] TreeRequest request) { var tree = await _ruleBuilderService.GetTreeAsync( request.EntityName, request.ViewName, request.ParentEntityName, request.Path ); return Ok(tree); } [HttpPost("test")] public async Task<IActionResult> TestRule([FromBody] TestRuleRequest request) { var result = await _ruleBuilderService.TestRuleAsync( request.Rule, request.TestData ); return Ok(result); } [HttpGet("contextual-data")] public async Task<IActionResult> GetContextualData([FromQuery] string field) { var data = await _ruleBuilderService.GetContextualDataAsync(field); return Ok(data); } } ``` #### Frontend Implementation ```typescript // Vue component <script setup lang="ts"> import { ref, onMounted } from 'vue' import { RuleBuilder, useRuleBuilderInstance } from 'vue-rule-builder' import type { DataProviders, RuleNodeDto } from 'vue-rule-builder' const myRule = ref<RuleNodeDto>({ id: 'root', kind: 0, combinator: 0, not: false, children: [] }) const dataProviders: DataProviders = { fetchBaseConfig: async ({ entityName, viewName, parentEntityName }) => { const response = await fetch('/api/rule-builder/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entityName, viewName, parentEntityName }) }) if (!response.ok) { throw new Error(`Failed to fetch config: ${response.statusText}`) } return response.json() }, fetchTree: async ({ entityName, viewName, parentEntityName, path }) => { const params = new URLSearchParams({ entityName }) if (viewName) params.append('viewName', viewName) if (parentEntityName) params.append('parentEntityName', parentEntityName) if (path) params.append('path', path) const response = await fetch(`/api/rule-builder/tree?${params}`) if (!response.ok) { throw new Error(`Failed to fetch tree: ${response.statusText}`) } return response.json() }, testRule: async (rule, testData) => { const response = await fetch('/api/rule-builder/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rule, testData }) }) if (!response.ok) { throw new Error(`Failed to test rule: ${response.statusText}`) } return response.json() }, fetchContextualData: async (fieldPath) => { const response = await fetch(`/api/rule-builder/contextual-data?field=${encodeURIComponent(fieldPath)}`) if (!response.ok) { throw new Error(`Failed to fetch contextual data: ${response.statusText}`) } return response.json() } } const handleValidation = (result: { valid: boolean; errors: string[] }) => { console.log('Validation result:', result) } const handleError = (error: Error) => { console.error('Rule builder error:', error) } </script> <template> <RuleBuilder v-model:rule="myRule" :entity-name="'Product'" :data-providers="dataProviders" @validation-change="handleValidation" @error="handleError" /> </template> ``` ### Error Handling #### Best Practices 1. **Always handle errors gracefully** 2. **Provide meaningful error messages** 3. **Implement retry logic for network failures** 4. **Log errors for debugging** #### Example Error Handling ```typescript const dataProviders: DataProviders = { fetchBaseConfig: async (params) => { try { const response = await fetch('/api/rule-builder/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params) }) if (!response.ok) { if (response.status === 404) { throw new Error('Configuration not found for this entity') } else if (response.status === 403) { throw new Error('Access denied to configuration') } else { throw new Error(`Server error: ${response.statusText}`) } } return response.json() } catch (error) { console.error('Failed to fetch base config:', error) // Return fallback configuration return { functions: [], operators: { comparison: [ { value: 'equals', label: 'Equals', supportedTypes: ['string', 'number'], arity: 2 }, { value: 'notEquals', label: 'Not Equals', supportedTypes: ['string', 'number'], arity: 2 } ], group: [ { value: 'AND', label: 'And' }, { value: 'OR', label: 'Or' } ] }, availableTabs: ['properties'], availableProperties: [] } } } } ``` ### Caching #### Implementation ```typescript class CachedDataProvider implements DataProviders { private cache = new Map<string, { data: any; timestamp: number }>() private readonly CACHE_TTL = 5 * 60 * 1000 // 5 minutes private getCacheKey(endpoint: string, params: any): string { return `${endpoint}:${JSON.stringify(params)}` } private isCacheValid(timestamp: number): boolean { return Date.now() - timestamp < this.CACHE_TTL } async fetchBaseConfig(params: any) { const key = this.getCacheKey('config', params) const cached = this.cache.get(key) if (cached && this.isCacheValid(cached.timestamp)) { return cached.data } const data = await this.fetchFromAPI('/api/rule-builder/config', params) this.cache.set(key, { data, timestamp: Date.now() }) return data } private async fetchFromAPI(endpoint: string, params: any) { // Implementation here } } ``` ### Performance Optimization #### Lazy Loading ```typescript const dataProviders: DataProviders = { fetchTree: async ({ entityName, path }) => { // Only fetch if path is provided (lazy loading) if (!path) { return [] } const response = await fetch(`/api/rule-builder/tree?entityName=${entityName}&path=${path}`) return response.json() } } ``` #### Debouncing ```typescript import { debounce } from 'lodash-es' const debouncedFetchTree = debounce(async (params) => { const response = await fetch('/api/rule-builder/tree', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params) }) return response.json() }, 300) const dataProviders: DataProviders = { fetchTree: debouncedFetchTree } ``` ### Testing #### Mock Implementation ```typescript const mockDataProviders: DataProviders = { fetchBaseConfig: async () => ({ functions: [ { name: 'calculateAge', returnType: 'number', parameters: [ { name: 'birthDate', type: 'date', required: true } ], description: 'Calculate age from birth date' } ], operators: { comparison: [ { value: 'equals', label: 'Equals', supportedTypes: ['string', 'number'], arity: 2 } ], group: [ { value: 'AND', label: 'And' }, { value: 'OR', label: 'Or' } ] }, availableTabs: ['properties', 'functions'], availableProperties: [] }), fetchTree: async () => [ { id: 'user', name: 'User', type: 'object', path: 'user', isExpandable: true, icon: 'User' } ], testRule: async (rule, testData) => ({ success: true, result: true, executionTime: 10 }), fetchContextualData: async () => [ { value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' } ] } ``` ## ๐ŸŽจ Customization ### Feature Flags Control which features are available in the rule builder: ```typescript interface FeatureFlags { enableRules?: boolean // Enable property-based rules enableExpressions?: boolean // Enable custom expressions enableReferences?: boolean // Enable rule references enableNotOperator?: boolean // Enable NOT operator enableDeveloperTools?: boolean // Enable developer console } ``` ### Theming The component uses CSS custom properties for theming: ```css :root { --rule-builder-primary: #3b82f6; --rule-builder-primary-hover: #2563eb; --rule-builder-danger: #ef4444; --rule-builder-danger-hover: #dc2626; --rule-builder-success: #10b981; --rule-builder-warning: #f59e0b; --rule-builder-background: #ffffff; --rule-builder-surface: #f9fafb; --rule-builder-border: #e5e7eb; --rule-builder-text: #111827; --rule-builder-text-muted: #6b7280; } ``` ### Custom Styling ```vue <template> <RuleBuilder class="my-rule-builder" v-model:rule="myRule" /> </template> <style> .my-rule-builder { --rule-builder-primary: #8b5cf6; --rule-builder-primary-hover: #7c3aed; } .my-rule-builder .rule-group { border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } </style> ``` ## ๐Ÿงช Testing ### Test Panel The rule builder includes a built-in test panel for validating rules: ```vue <template> <RuleBuilder v-model:rule="myRule" :data-providers="dataProviders" /> </template> <script setup> const dataProviders = { testRule: async (rule, testData) => { // Your test implementation const response = await fetch('/api/rules/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rule, testData }) }) return response.json() } } </script> ``` ### Validation ```typescript const store = useRuleBuilderStore() const validation = store.validateRules(myRule.value) if (!validation.isValid) { console.log('Validation errors:', validation.errors) } ``` ## ๐Ÿ“Š Data Structures ### RuleNodeDto The core data structure representing a rule: ```typescript interface RuleNodeDto { id: string kind: NodeKind // 0: Group, 1: Rule, 2: Expression, 3: Reference not: boolean // NOT operator children: RuleNodeDto[] // Child nodes combinator?: CombinatorKind // AND/OR operator for groups field?: string // Field path for property rules operator?: string // Comparison operator values?: ParameterValueDto[] // Rule values expression?: string // Custom expression function?: FunctionCallDto // Function call for property rules referenceId?: string // Reference ID for reference rules referenceName?: string // Reference name for display } ``` ### TreeNode Field definition structure: ```typescript interface TreeNode { id: string // Unique identifier name: string // Display name type: string // Data type: 'string', 'number', 'boolean', 'date', 'object', 'array' path: string // Property path (e.g., 'user.email') isExpandable: boolean // Whether this node can be expanded icon?: string // Icon name (optional) parentId?: string // Parent node ID (for child nodes) children?: TreeNode[] // Child nodes (for expandable nodes) capabilities?: number // Field capabilities for operator filtering } ``` ### ValidationResult Validation result structure: ```typescript interface ValidationResult { isValid: boolean errors: Array<{ message: string path?: string nodeId?: string }> } ``` ### FunctionCallDto Function definition structure: ```typescript interface FunctionCallDto { name: string returnType: string parameters?: Array<{ name: string type: string required: boolean description?: string }> description?: string example?: string category?: string } ``` ### OperatorInfo Operator definition structure: ```typescript interface OperatorInfo { value: string label: string supportedTypes: string[] arity: OperatorArity // Unary, Binary, Ternary, Variadic description?: string } ``` ## ๐Ÿ”„ Advanced Usage ### Multi-Instance Support ```vue <template> <div> <RuleBuilder :instance-id="'user-rules'" v-model:rule="userRules" /> <RuleBuilder :instance-id="'product-rules'" v-model:rule="productRules" /> </div> </template> <script setup> import { ref } from 'vue' import { RuleBuilder } from 'vue-rule-builder' const userRules = ref({ /* ... */ }) const productRules = ref({ /* ... */ }) </script> ``` ### Custom Operators ```typescript const customOperators = [ { value: 'customOperator', label: 'Custom Operator', supportedTypes: ['string', 'number'], arity: 2 // Binary operator } ] const dataProviders = { fetchBaseConfig: async () => ({ operators: { comparison: customOperators } }) } ``` ### Custom Functions ```typescript const customFunctions = [ { name: 'calculateAge', returnType: 'number', parameters: [ { name: 'birthDate', type: 'date', required: true } ], description: 'Calculate age from birth date' } ] const dataProviders = { fetchBaseConfig: async () => ({ functions: customFunctions }) } ``` ## ๐Ÿค Contributing 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/amazing-feature` 3. Commit your changes: `git commit -m 'Add amazing feature'` 4. Push to the branch: `git push origin feature/amazing-feature` 5. Open a Pull Request ## ๐Ÿ“„ License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## ๐Ÿ†˜ Support - ๐Ÿ“– [Documentation](https://github.com/your-org/vue-rule-builder) - ๐Ÿ› [Issue Tracker](https://github.com/your-org/vue-rule-builder/issues) - ๐Ÿ’ฌ [Discussions](https://github.com/your-org/vue-rule-builder/discussions) --- Made with โค๏ธ by the Vue Rule Builder team