@praxisui/visual-builder
Version:
Visual rule and expression builder for Praxis UI with mini-DSL support, validation and context variables.
658 lines (473 loc) • 19.5 kB
Markdown
# Praxis Visual Builder
## 🔰 Exemplos / Quickstart
Para ver esta biblioteca em funcionamento em uma aplicação completa, utilize o projeto de exemplo (Quickstart):
- Repositório: https://github.com/codexrodrigues/praxis-ui-quickstart
- O Quickstart demonstra a integração das bibliotecas `@praxisui/*` em um app Angular, incluindo instalação, configuração e uso em telas reais.
A comprehensive Angular library for building visual rule editors with support for mini-DSL expressions and contextual specifications.
## Features
### 🎯 **Core Components**
- **ExpressionEditorComponent**: Advanced DSL expression editor with syntax highlighting and autocomplete
- **ContextVariableManagerComponent**: Complete context variable management system
- **SpecificationBridgeService**: Bidirectional conversion between visual rules and DSL expressions
### ⚡ **Advanced Capabilities**
- **Mini-DSL Support**: Full DSL parsing, validation, and round-trip conversion
- **Context Variables**: Dynamic variable resolution with scoped contexts
- **Real-time Validation**: Live syntax checking with performance metrics
- **Autocomplete**: Intelligent suggestions for fields, functions, operators, and variables
- **Round-trip Integrity**: Seamless conversion between visual and textual representations
## Installation
```bash
npm install @praxisui/visual-builder
```
## Quick Start
### Basic Expression Editor
```typescript
import { ExpressionEditorComponent } from "@praxisui/visual-builder";
@Component({
template: ` <praxis-expression-editor [fieldSchemas]="fieldSchemas" [contextVariables]="contextVariables" [(expression)]="expression" (validationChange)="onValidationChange($event)"> </praxis-expression-editor> `,
})
export class MyComponent {
fieldSchemas = [
{ name: "age", type: "number", label: "Age" },
{ name: "name", type: "string", label: "Name" },
{ name: "active", type: "boolean", label: "Active" },
];
contextVariables = [{ name: "user.minAge", type: "number", scope: "user", example: "18" }];
expression = "age > ${user.minAge} && active == true";
onValidationChange(result: ExpressionValidationResult) {
console.log("Expression valid:", result.isValid);
console.log("Issues:", result.issues);
}
}
```
### Context Variable Management
```typescript
import { ContextVariableManagerComponent } from "@praxisui/visual-builder";
@Component({
template: ` <praxis-context-variable-manager [(contextVariables)]="contextVariables" [allowedScopes]="allowedScopes" (variableAdd)="onVariableAdd($event)" (variableUpdate)="onVariableUpdate($event)"> </praxis-context-variable-manager> `,
})
export class VariableManagementComponent {
contextVariables: EnhancedContextVariable[] = [];
allowedScopes = ["user", "session", "global"];
onVariableAdd(variable: EnhancedContextVariable) {
console.log("New variable added:", variable);
}
}
```
### Visual Rule Editor in Embedded Mode
```typescript
import { RuleEditorComponent } from "@praxisui/visual-builder";
@Component({
template: ` <praxis-rule-editor [config]="builderConfig" [embedded]="true" (rulesChanged)="onRulesChanged($event)"> </praxis-rule-editor> `,
})
export class EmbeddedRuleEditor {
builderConfig = {
/* ... */
};
onRulesChanged(rules: any) {
console.log("Updated rules:", rules);
}
}
```
`embedded` mode allows the editor to fit within existing layouts without spawning nested scrollbars, making it suitable for corporate dashboards and configuration panels where space is constrained.
## Mini-DSL Language Guide
### Basic Syntax
The mini-DSL supports a rich expression language for building validation rules and conditions.
#### Field References
```dsl
age > 18
name == "John Doe"
active == true
score >= 80
```
#### Context Variables
Context variables are referenced using `${scope.variable}` syntax:
```dsl
age > ${user.minAge}
department == "${user.department}"
level >= ${policy.requiredLevel}
```
#### Operators
**Comparison Operators:**
```dsl
age == 25 # Equality
age != 30 # Inequality
age > 18 # Greater than
age >= 21 # Greater than or equal
age < 65 # Less than
age <= 64 # Less than or equal
```
**Logical Operators:**
```dsl
age > 18 && active == true # AND
priority == "high" || urgent == true # OR
!(deleted == true) # NOT
status == "A" ^ backup == true # XOR (exclusive or)
qualified == true => approved == true # IMPLIES
```
**Membership Operators:**
```dsl
category in ["tech", "science", "math"] # IN (array membership)
role not in ["guest", "anonymous"] # NOT IN
```
**Null Checks:**
```dsl
description != null # Not null
optional == null # Is null
```
#### Functions
**String Functions:**
```dsl
contains(tags, "important") # Check if string/array contains value
startsWith(email, "admin") # Check if string starts with value
endsWith(filename, ".pdf") # Check if string ends with value
length(description) > 10 # Get string/array length
upper(category) == "TECHNOLOGY" # Convert to uppercase
lower(name) == "john" # Convert to lowercase
```
**Collection Functions:**
```dsl
atLeast(2, [condition1, condition2, condition3]) # At least N conditions true
exactly(1, [optionA, optionB, optionC]) # Exactly N conditions true
forEach(items, itemCondition) # All items satisfy condition
uniqueBy(collection, ["field1", "field2"]) # Unique by specified fields
minLength(array, 3) # Minimum array length
maxLength(array, 10) # Maximum array length
```
**Conditional Functions:**
```dsl
requiredIf(field, condition) # Field required if condition true
visibleIf(field, condition) # Field visible if condition true
disabledIf(field, condition) # Field disabled if condition true
readonlyIf(field, condition) # Field readonly if condition true
```
**Optional Handling:**
```dsl
ifDefined(optionalField, condition) # Apply condition only if field defined
ifNotNull(field, condition) # Apply condition only if field not null
ifExists(field, condition) # Apply condition only if field exists
withDefault(field, defaultValue, condition) # Use default if field undefined
```
### Complex Expressions
#### Nested Conditions
```dsl
((age >= 18 && age <= 65) || experience > 10) &&
(department == "Engineering" || department == "Product") &&
!(archived == true || deleted == true)
```
#### Mixed Context and Fields
```dsl
salary >= ${policy.minSalary} &&
salary <= ${policy.maxSalary} &&
location in ${user.allowedLocations} &&
startDate >= "${session.currentDate}"
```
#### Function Composition
```dsl
contains(upper(department), "ENG") &&
length(trim(description)) > ${config.minDescLength} &&
endsWith(lower(email), "@company.com")
```
### Context Variables
Context variables provide dynamic values that can be resolved at runtime based on different scopes.
#### Scopes
**User Scope** (`user.*`):
- User-specific values (preferences, profile data)
- Example: `${user.department}`, `${user.role}`, `${user.level}`
**Session Scope** (`session.*`):
- Session-specific values (temporary data, current state)
- Example: `${session.currentDate}`, `${session.userId}`, `${session.locale}`
**Environment Scope** (`env.*`):
- Environment configuration (debug flags, feature toggles)
- Example: `${env.debug}`, `${env.features.newUI}`, `${env.apiVersion}`
**Global Scope** (`global.*`):
- Application-wide constants (policies, configurations)
- Example: `${global.minAge}`, `${global.maxFileSize}`, `${global.supportedFormats}`
#### Variable Types
**String Variables:**
```dsl
name == "${user.fullName}"
department == "${user.department}"
```
**Number Variables:**
```dsl
age > ${user.minAge}
salary >= ${policy.baseSalary}
```
**Boolean Variables:**
```dsl
active == ${user.isActive}
debug == ${env.debugMode}
```
**Array Variables:**
```dsl
category in ${user.allowedCategories}
format in ${global.supportedFormats}
```
**Date Variables:**
```dsl
createdDate >= "${session.startDate}"
expirationDate <= "${policy.maxExpirationDate}"
```
### Validation and Error Handling
#### Syntax Errors
The DSL parser provides detailed error messages for syntax issues:
```typescript
// Invalid syntax examples and their error messages:
// "age >" -> "Missing operand after comparison operator"
// "age >> 18" -> "Unknown operator '>>'"
// "(age > 18" -> "Unclosed parentheses"
// "unknownFunc(age)" -> "Unknown function 'unknownFunc'"
```
#### Field Validation
```typescript
// Unknown field warnings:
// "unknownField == 'test'" -> "Unknown field: unknownField. Did you mean 'knownField'?"
```
#### Context Variable Validation
```typescript
// Missing context variable warnings:
// "age > ${missing.variable}" -> "Unknown context variable: missing.variable"
// With suggestions: "Did you mean 'existing.variable'?"
```
#### Performance Warnings
```typescript
// Complex expression warnings:
// For expressions with >50 operators:
// "Expression complexity is high (67 operators). Consider breaking into smaller expressions."
```
## API Reference
### Exports
Main exports available from `@praxisui/visual-builder` (see `projects/praxis-visual-builder/src/public-api.ts`):
- Components: `RuleEditorComponent`, `RuleCanvasComponent`, `RuleNodeComponent`, `FieldConditionEditorComponent`, `ConditionalValidatorEditorComponent`, `CollectionValidatorEditorComponent`, `MetadataEditorComponent`, `DslViewerComponent`, `JsonViewerComponent`, `RoundTripTesterComponent`, `ExportDialogComponent`, `DslLinterComponent`, `VisualRuleBuilderComponent`, `TemplateGalleryComponent`, `TemplateEditorDialogComponent`, `TemplatePreviewDialogComponent`
- Services: `SpecificationBridgeService`, `RuleBuilderService`, `RoundTripValidatorService`, `ExportIntegrationService`, `WebhookIntegrationService`, `RuleTemplateService`, `RuleValidationService`, `RuleNodeRegistryService`, `ContextManagementService`, `FieldSchemaService`, `RuleConversionService`, `DslParsingService`
- Models/Types: `FieldSchema`, `RuleBuilderConfig`, `ArrayFieldSchema`, `ContextScope`, `ContextEntry`, `ContextValue`, `SpecificationContextualConfig`
- Errors: `VisualBuilderError`, `VBValidationError`, `ConversionError`, `RegistryError`, `DslError`, `ContextError`, `ConfigurationError`, `InternalError`, `ErrorCategory`, `ErrorSeverity`, `ErrorHandler`, `globalErrorHandler`, `createError`, `ErrorInfo`, `ErrorStatistics`
### ExpressionEditorComponent
#### Inputs
```typescript
@Input() expression: string = ''; // Current DSL expression
@Input() fieldSchemas: FieldSchema[] = []; // Available fields for autocomplete
@Input() contextVariables: ContextVariable[] = []; // Available context variables
@Input() functionRegistry?: FunctionRegistry; // Custom function registry
@Input() validationConfig?: ValidationConfig; // Validation configuration
@Input() editorOptions?: EditorOptions; // Editor display options
```
#### Outputs
```typescript
@Output() expressionChange = new EventEmitter<string>(); // Expression changes
@Output() validationChange = new EventEmitter<ExpressionValidationResult>(); // Validation updates
@Output() suggestionRequest = new EventEmitter<SuggestionContext>(); // Autocomplete requests
@Output() focusChange = new EventEmitter<boolean>(); // Focus state changes
```
#### Methods
```typescript
// Get autocomplete suggestions
getSuggestions(text: string, position: number): Promise<DslSuggestion[]>
// Validate current expression
validateExpression(): Promise<ExpressionValidationResult>
// Insert text at cursor position
insertTextAtCursor(text: string): void
// Format expression
formatExpression(): void
// Clear expression
clearExpression(): void
```
### ContextVariableManagerComponent
#### Inputs
```typescript
@Input() contextVariables: EnhancedContextVariable[] = []; // Current variables
@Input() allowedScopes: ContextScope[] = []; // Allowed variable scopes
@Input() readOnly: boolean = false; // Read-only mode
@Input() showCategories: boolean = true; // Show category grouping
@Input() allowImportExport: boolean = true; // Enable import/export
```
#### Outputs
```typescript
@Output() contextVariablesChange = new EventEmitter<EnhancedContextVariable[]>(); // Variables updated
@Output() variableAdd = new EventEmitter<EnhancedContextVariable>(); // Variable added
@Output() variableUpdate = new EventEmitter<EnhancedContextVariable>(); // Variable updated
@Output() variableDelete = new EventEmitter<string>(); // Variable deleted
@Output() importComplete = new EventEmitter<ImportResult>(); // Import completed
```
#### Methods
```typescript
// Add new variable
addVariable(variable: Partial<EnhancedContextVariable>): void
// Update existing variable
updateVariable(id: string, updates: Partial<EnhancedContextVariable>): void
// Delete variable
deleteVariable(id: string): void
// Get variables by scope
getVariablesByScope(scope: string): EnhancedContextVariable[]
// Export variables
exportVariables(format: 'json' | 'csv'): string
// Import variables
importVariables(data: string, format: 'json' | 'csv'): Promise<ImportResult>
// Validate variable name
isValidVariableName(name: string): boolean
```
### SpecificationBridgeService
#### DSL Expression Methods
```typescript
// Parse DSL expression to specification
parseDslExpression<T>(expression: string, config?: DslParsingConfig): DslParsingResult<T>
// Validate expression round-trip integrity
validateExpressionRoundTrip<T>(expression: string, config?: DslParsingConfig): RoundTripResult
// Create contextual specification
createContextualSpecification<T>(template: string, config?: ContextualConfig): ContextualSpecification<T>
// Resolve context tokens in template
resolveContextTokens(template: string, variables: ContextVariable[]): string
// Extract context tokens from template
extractContextTokens(template: string): string[]
// Validate context tokens
validateContextTokens(template: string, variables: ContextVariable[]): ValidationIssue[]
```
#### Rule Node Conversion Methods
```typescript
// Convert rule node to specification
ruleNodeToSpecification<T>(node: RuleNode): Specification<T>
// Convert specification to rule node
specificationToRuleNode<T>(spec: Specification<T>): RuleNode
// Export rule node to DSL
exportToDsl<T>(node: RuleNode, options?: ExportOptions): string
// Validate round-trip through rule nodes
validateRoundTrip<T>(node: RuleNode): RoundTripValidationResult
```
#### Context Provider Methods
```typescript
// Update context provider
updateContextProvider(provider: ContextProvider): void
// Get current context provider
getContextProvider(): ContextProvider | undefined
```
## Advanced Usage
### Custom Function Registry
```typescript
import { FunctionRegistry } from '@praxisui/specification';
// Create custom function registry
const customRegistry = new FunctionRegistry();
// Register custom functions
customRegistry.register('isEmail', {
name: 'isEmail',
arity: 1,
implementation: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
description: 'Validates email format'
});
customRegistry.register('dateAdd', {
name: 'dateAdd',
arity: 3,
implementation: (date: Date, amount: number, unit: string) => {
// Implementation for date arithmetic
},
description: 'Adds time to a date'
});
// Use in expression editor
<praxis-expression-editor
[functionRegistry]="customRegistry"
expression="isEmail(userEmail) && dateAdd(startDate, 30, 'days') > now()">
</praxis-expression-editor>
```
### Custom Context Provider
```typescript
import { ContextProvider } from "@praxisui/specification";
class DatabaseContextProvider implements ContextProvider {
private cache = new Map<string, any>();
async getValue(key: string): Promise<any> {
if (this.cache.has(key)) {
return this.cache.get(key);
}
// Fetch from database or API
const value = await this.fetchFromDatabase(key);
this.cache.set(key, value);
return value;
}
hasValue(key: string): boolean {
return this.cache.has(key) || this.isValidKey(key);
}
setValue(key: string, value: any): void {
this.cache.set(key, value);
}
// ... other methods
}
// Use custom provider
const provider = new DatabaseContextProvider();
this.bridgeService.updateContextProvider(provider);
```
### Integration with Form Builders
```typescript
@Component({
template: `
<form [formGroup]="ruleForm">
<!-- Visual Rule Builder -->
<praxis-expression-editor formControlName="expression" [fieldSchemas]="formFieldSchemas" [contextVariables]="availableVariables"> </praxis-expression-editor>
<!-- Context Variable Management -->
<praxis-context-variable-manager [(contextVariables)]="availableVariables" [allowedScopes]="['user', 'session']"> </praxis-context-variable-manager>
<!-- Generated Rule Preview -->
<pre>{{ generatedRule | json }}</pre>
</form>
`,
})
export class RuleBuilderFormComponent {
ruleForm = this.fb.group({
expression: ["", Validators.required],
metadata: this.fb.group({
name: ["", Validators.required],
description: [""],
priority: [1],
}),
});
get generatedRule() {
const expression = this.ruleForm.get("expression")?.value;
if (!expression) return null;
const parseResult = this.bridgeService.parseDslExpression(expression, {
knownFields: this.formFieldSchemas.map((f) => f.name),
});
return parseResult.success ? parseResult.specification?.toJSON() : null;
}
}
```
## Performance Guidelines
### Optimization Tips
1. **Field Schema Management**:
```typescript
// ✅ Good: Reuse field schema objects
const fieldSchemas = useMemo(() => fields.map((f) => ({ name: f.name, type: f.type, label: f.label })), [fields]);
// ❌ Avoid: Creating new objects on every render
const fieldSchemas = fields.map((f) => ({ name: f.name, type: f.type }));
```
2. **Expression Validation**:
```typescript
// ✅ Good: Debounce validation
const debouncedValidation = useMemo(
() => debounce(validateExpression, 300),
[]
);
// ❌ Avoid: Validating on every keystroke
onChange={(expr) => validateExpression(expr)}
```
## Building
To build the library, run:
```bash
ng build praxis-visual-builder
```
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
### Publishing the Library
Once the project is built, you can publish your library by following these steps:
1. Navigate to the `dist` directory:
```bash
cd dist/praxis-visual-builder
```
2. Run the `npm publish` command to publish your library to the npm registry:
```bash
npm publish
```
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test praxis-visual-builder
```
## Contributing
Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
## License
Apache-2.0 – see the `LICENSE` packaged with this library or the repository root.