From f36ba2b057d4724f88b75855178b2dd901f49721 Mon Sep 17 00:00:00 2001 From: Guillaume Fontorbe Date: Tue, 8 Jul 2025 14:07:21 +0200 Subject: [PATCH 1/5] Core concepts documentation --- .../docs/concepts/architecture-overview.md | 49 ++ hugo/content/docs/concepts/best-practices.md | 398 ++++++++++ hugo/content/docs/concepts/core-components.md | 86 ++ hugo/content/docs/concepts/data-flow.md | 32 + .../content/docs/concepts/extension-points.md | 748 ++++++++++++++++++ .../docs/recipes/architecture-overview.md | 102 --- .../docs/recipes/custom-interactions.md | 4 +- .../docs/recipes/dependency-injection.md | 128 --- hugo/content/docs/recipes/micro-layout.md | 4 +- hugo/content/docs/recipes/model-sources.md | 170 ---- hugo/content/docs/recipes/svg-rendering.md | 2 +- hugo/content/docs/ref/features.md | 2 +- hugo/content/docs/ref/smodel.md | 2 +- hugo/data/menu/main.yaml | 18 +- 14 files changed, 1332 insertions(+), 413 deletions(-) create mode 100644 hugo/content/docs/concepts/architecture-overview.md create mode 100644 hugo/content/docs/concepts/best-practices.md create mode 100644 hugo/content/docs/concepts/core-components.md create mode 100644 hugo/content/docs/concepts/data-flow.md create mode 100644 hugo/content/docs/concepts/extension-points.md delete mode 100644 hugo/content/docs/recipes/architecture-overview.md delete mode 100644 hugo/content/docs/recipes/dependency-injection.md delete mode 100644 hugo/content/docs/recipes/model-sources.md diff --git a/hugo/content/docs/concepts/architecture-overview.md b/hugo/content/docs/concepts/architecture-overview.md new file mode 100644 index 0000000..bb39b7e --- /dev/null +++ b/hugo/content/docs/concepts/architecture-overview.md @@ -0,0 +1,49 @@ +--- +title: 'Architecture Overview' +--- + +{{< toc >}} + +Sprotty is a diagramming framework that provides a robust foundation for building interactive, web-based diagram editors. At its core, Sprotty follows a clear architectural pattern that ensures maintainability, testability, and extensibility. + +## Introduction to Sprotty's Architecture + +The foundation of Sprotty's architecture is built around three main components that work together in a unidirectional flow: + +1. **Action Dispatcher**: The central hub that receives and processes all diagram operations +2. **Command Stack**: The state manager that maintains the diagram's history and current state +3. **Viewer**: The renderer that transforms the internal model into a visual representation + +These components form a cyclic flow of information that prevents feedback loops and ensures predictable behavior. + +{{< mermaid class="text-center">}} +flowchart TD; + ActionDispatcher[Action Dispatcher] + CommandStack[Command Stack] + Viewer[Viewer] + ActionDispatcher -->|Command| CommandStack + CommandStack -->|SModel| Viewer + Viewer -->|Action| ActionDispatcher +{{< /mermaid>}} + +This architecture provides several key benefits: + +- **Predictable State Management**: The unidirectional flow makes it easy to track and debug state changes +- **Testability**: Each component has a clear responsibility and can be tested in isolation +- **Extensibility**: New features can be added by extending any of the core components +- **Performance**: The architecture minimizes unnecessary updates and optimizes rendering + +## What You'll Learn + +This documentation series will guide you through Sprotty's architecture in logical steps: + +1. **[Core Components]({{< relref "core-components" >}})**: Deep dive into the three main architectural components +2. **[Data Flow]({{< relref "data-flow" >}})**: Understanding how information moves through the system +3. **[Extension Points]({{< relref "extension-points" >}})**: How to customize and extend Sprotty's functionality +4. **[Best Practices]({{< relref "best-practices" >}})**: Guidelines for building robust Sprotty applications + +Each section builds upon the previous ones, providing a comprehensive understanding of how to build effective diagramming applications with Sprotty. + +## Next Steps + +Start with the [Core Components]({{< relref "core-components" >}}) section to understand the fundamental building blocks of Sprotty's architecture, then progress through each section to build a complete understanding of the framework. diff --git a/hugo/content/docs/concepts/best-practices.md b/hugo/content/docs/concepts/best-practices.md new file mode 100644 index 0000000..4efc665 --- /dev/null +++ b/hugo/content/docs/concepts/best-practices.md @@ -0,0 +1,398 @@ +--- +title: 'Best Practices' +weight: 5 +--- + +{{< toc >}} + +Now that you understand Sprotty's [Extension Points]({{< relref "extension-points" >}}), let's explore the best practices for building robust, maintainable, and performant diagramming applications. These guidelines will help you avoid common pitfalls and create high-quality Sprotty-based solutions. + +## Architecture & Design Patterns + +### Dependency Injection (DI) Configuration + +- **Use InversifyJS properly:** Always use `@injectable` decorators for your classes and configure them in DI modules +- **Use utility functions for your modules:** Use utility functions like `configureModelElement()`, `configureCommand()`, `configureActionHandler()`, etc. to configure your DI modules +- **Load modules in the correct order:** Start with `loadDefaultModules()` then add your custom modules + +**✅ Always use proper DI configuration:** + +```typescript +// Good: Proper DI setup with type mapping +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + // Map model types to implementations and views + configureModelElement(context, 'task', RectangularNode, TaskNodeView); + configureModelElement(context, 'edge', SEdgeImpl, PolylineEdgeView); + + // Bind services with proper scoping + bind(TYPES.ModelSource).to(CustomModelSource).inSingletonScope(); + bind(TYPES.IActionDispatcher).to(ActionDispatcher).inSingletonScope(); + + // Configure viewer options + configureViewerOptions(context, { + needsClientLayout: true, + baseDiv: 'sprotty-diagram' + }); +}); + +// Load modules in correct order +const container = new Container(); +loadDefaultModules(container); +container.load(customModule); +``` + +**❌ Avoid direct instantiation:** + +```typescript +// Bad: Direct instantiation makes testing difficult +private actionDispatcher = new ActionDispatcher(); +private modelSource = new LocalModelSource(); +``` + +### Model Design Principles + +- **Extend existing interfaces:** Create custom types by extending Sprotty's base interfaces (SNode, SEdge, etc.) +- **Use JSON-serializable models:** Ensure your model elements can be serialized for client-server communication +- **Implement proper ID management:** Use unique, consistent IDs for all elements +- **Reserve system properties:** Don't override reserved properties like children, parent, index + +**✅ Extend existing interfaces properly:** + +```typescript +// Good: Extend base interfaces for custom types +export interface TaskNode extends SNode { + name: string; + isRunning: boolean; + isFinished: boolean; + priority?: 'low' | 'medium' | 'high'; +} + +// Good: Use JSON-serializable models +export const taskModel: SGraph = { + type: 'graph', + id: 'task-graph', + children: [ + { + type: 'task', + id: 'task-1', + name: 'First Task', + isRunning: false, + isFinished: true, + position: { x: 0, y: 0 }, + size: { width: 100, height: 50 } + } + ] +}; +``` + +**❌ Don't override reserved properties:** + +```typescript +// Bad: Overriding reserved properties +const node = { + type: 'task', + id: 'task-1', + children: 'invalid', // Reserved property + parent: 'invalid', // Reserved property + index: 'invalid' // Reserved property +}; +``` + +## Performance & Scalability + +### Large Diagram Optimization + +- **Use view filtering:** Implement conditional rendering for large datasets +- **Leverage projection features:** Use projection bars for navigation in large diagrams +- **Implement lazy loading:** Load and/or display diagram sections on demand +- **Optimize bounds computation:** Provide valid bounds in your model to avoid expensive calculations + +**✅ Implement conditional rendering for large datasets:** + +```typescript +// Good: Use ThunkView for performance optimization +@injectable() +export class OptimizedNodeView extends ThunkView { + watchedArgs(model: SNode): any[] { + return [model.isVisible, model.zoom, model.position]; + } + + selector(model: SNode): string { + return 'g'; + } + + isVisible(model: SNode): boolean { + // Only render when zoomed in enough + return model.zoom * model.size.width > 10; + } + + render(model: SNode, context: RenderingContext): VNode { + return + + {context.renderChildren(model)} + ; + } +} +``` + +**✅ Use projection features for navigation:** + +```typescript +// Good: Implement projection bars for large diagrams +export class ProjectionBarView implements IView { + render(model: ViewportRootElement, context: RenderingContext): VNode { + const projections = this.getProjections(model); + return
+ {projections.map(p => this.renderProjection(p, model))} +
; + } +} +``` + +### Memory Management + +- **Set undo history limits:** Configure `undoHistoryLimit` in `CommandStackOptions` to prevent memory leaks +- **Clean up resources:** Properly dispose of event listeners and references + +**✅ Configure proper undo history limits:** + +```typescript +// Good: Set undo history limits to prevent memory leaks, this is the default configuration +const commandStackOptions: CommandStackOptions = { + defaultDuration: 250, + undoHistoryLimit: 50 // Prevent unlimited growth, a negative number results in memory leak +}; + +// In DI module +configureCommandStackOptions(context, commandStackOptions) +``` + +**✅ Implement proper cleanup:** + +```typescript +// Good: Clean up resources properly +export class CustomModelSource extends LocalModelSource { + private eventListeners: Array<() => void> = []; + + addEventListener(element: HTMLElement, event: string, handler: EventListener) { + element.addEventListener(event, handler); + this.eventListeners.push(() => element.removeEventListener(event, handler)); + } + + dispose() { + this.eventListeners.forEach(cleanup => cleanup()); + this.eventListeners = []; + } +} +``` + +## Security & Validation + +- **Implement label validators:** Use `IEditLabelValidator` for user input validation +- **Sanitize HTML content:** When using ForeignObjectElement, validate and sanitize HTML content +- **Validate model data:** Ensure incoming model data is properly validated before processing + +**✅ Implement comprehensive label validation:** + +```typescript +// Good: Robust input validation +@injectable() +export class TaskLabelValidator implements IEditLabelValidator { + async validate(value: string, label: EditableLabel): Promise { + // Length validation + if (value.length === 0) { + return { severity: 'error', message: 'Label cannot be empty' }; + } + + if (value.length > 100) { + return { severity: 'error', message: 'Label too long (max 100 characters)' }; + } + + // Content validation + if (!/^[a-zA-Z0-9\s\-_]+$/.test(value)) { + return { severity: 'warning', message: 'Label contains special characters' }; + } + + // Duplicate validation + if (await this.isDuplicate(value, label)) { + return { severity: 'error', message: 'Label already exists' }; + } + + return { severity: 'ok' }; + } +} +``` + +**✅ Validate model data and handle HTML content safely:** + +```typescript +// Good: Validate model structure and sanitize HTML content +export class SafeModelSource extends LocalModelSource { + protected validateModel(model: SModelRoot): void { + // Check for required properties + if (!model.type || !model.id) { + throw new Error('Model must have type and id properties'); + } + + // Validate children recursively + if (model.children) { + this.validateChildren(model.children); + } + } + + private validateChildren(children: SModelElement[]): void { + const seenIds = new Set(); + + for (const child of children) { + // Check for duplicate IDs (Sprotty throws error for this) + if (seenIds.has(child.id)) { + throw new Error(`Duplicate ID found: ${child.id}`); + } + seenIds.add(child.id); + + // Validate HTML content in ForeignObjectElement + if (child.type === 'foreignObject' && 'code' in child) { + this.sanitizeHtmlContent((child as any).code); + } + + // Recursively validate nested children + if (child.children) { + this.validateChildren(child.children); + } + } + } + + private sanitizeHtmlContent(htmlContent: string): string { + // Remove script tags and event handlers + const sanitized = htmlContent + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/\bon\w+\s*=/gi, '') // Remove event handlers + .replace(/javascript:/gi, '') // Remove javascript: protocol + .replace(/data:text\/html/gi, ''); // Remove data URLs + + return sanitized; + } + + async requestModel(): Promise { + try { + const model = await this.loadModel(); + this.validateModel(model); + this.updateModel(model); + } catch (error) { + console.error('Model validation failed:', error); + // Provide fallback or user feedback + this.handleValidationError(error); + } + } +} +``` + +## User Experience & Accessibility + +### Responsive Design + +- **Handle viewport changes:** Implement proper zoom and pan limits +- **Use CSS for styling:** Leverage CSS classes for consistent styling across different states +- **Provide visual feedback:** Implement hover, selection, and focus states + +**✅ Implement proper viewport handling:** + +```typescript +// Good: Responsive viewport configuration +configureViewerOptions(context, { + needsClientLayout: true, + baseDiv: 'sprotty-diagram', + zoomLimits: { min: 0.1, max: 5.0 }, + horizontalScrollLimits: { min: -10000, max: 10000 }, + verticalScrollLimits: { min: -10000, max: 10000 } +}); +``` + +**✅ Use CSS for consistent styling:** + +```css +/* Good: Responsive and accessible styling */ +.sprotty-node.task { + fill: #c0e0fc; + stroke: #444; + stroke-width: 1; + transition: all 0.2s ease; + cursor: pointer; +} + +.sprotty-node.task:hover { + stroke-width: 2; + filter: brightness(1.1); +} + +.sprotty-node.task.selected { + stroke-width: 3; + stroke: #0066cc; +} + +.sprotty-node.task.running { + fill: #ff6b6b; + animation: pulse 2s infinite; +} + +.sprotty-node.task.finished { + fill: #51cf66; +} + +/* Accessibility: High contrast mode support */ +@media (prefers-contrast: high) { + .sprotty-node.task { + stroke: #000; + stroke-width: 2; + } +} +``` + +### Accessibility Features + +- **Add ARIA labels:** Include proper accessibility attributes for screen readers +- **Keyboard navigation:** Implement keyboard shortcuts and navigation +- **Color contrast:** Ensure sufficient color contrast for text and interactive elements + +**✅ Add ARIA attributes and keyboard navigation:** + +```typescript +// Good: Accessible view implementation +export class AccessibleNodeView extends SShapeElementView { + render(element: SNode, context: RenderingContext): VNode { + return this.handleKeyDown(e, element)} + onfocus={(e) => this.handleFocus(e, element)} + onblur={(e) => this.handleBlur(e, element)}> + + + {element.name} + + ; + } + + private handleKeyDown(event: KeyboardEvent, element: SNode): void { + switch (event.key) { + case 'Enter': + case ' ': + this.actionDispatcher.dispatch(new SelectElementAction([element.id])); + event.preventDefault(); + break; + case 'Escape': + this.actionDispatcher.dispatch(new ClearSelectionAction()); + event.preventDefault(); + break; + } + } +} +``` + +## Next Steps + +Remember that Sprotty is designed to be flexible and extensible. These best practices provide a solid foundation, but don't hesitate to adapt them to your specific use case while maintaining the core architectural principles. diff --git a/hugo/content/docs/concepts/core-components.md b/hugo/content/docs/concepts/core-components.md new file mode 100644 index 0000000..44f16df --- /dev/null +++ b/hugo/content/docs/concepts/core-components.md @@ -0,0 +1,86 @@ +--- +title: 'Core Components' +--- + +{{< toc >}} + +Let's dive into the four components that form the foundation of Sprotty's architecture. Understanding these components is key for building effective diagramming applications. + +## Model Source + + The **Model Source** serves as the entry point for diagram data and manages the lifecycle of the diagram model. It acts as a bridge between data sources and the Sprotty event cycle. + + **Key responsibilities:** + +- Model management +- Action processing + +Sprotty provides two types of Model Source to accommodate different use cases": + +### LocalModelSource + +The `LocalModelSource` manages the model directly in the client, providing a facade over the action-based API. + +**Key Features**: + +- Direct model control +- Handle actions locally + +**Use Cases:** + +- Standalone diagram applications +- Offline-capable applications +- Client-side only diagramming tools +- Fast prototyping and demos + +### DiagramServerProxy + +The `DiagramServerProxy` connects to remote data sources and manages client-server communication. + +**Key Features:** + +- Remote communication with server +- Holds a `clientId` for multi-client scenarios +- Can handle actions locally or forward them to the server + +**Use Cases:** + +- Collaborative diagram editors +- Integration with backend systems +- Multi-users environment +- Cloud-based diagramming applications +- IDE Extensions (e.g. VSCode Extensions) + +## Action Dispatcher + +The **Action Dispatcher** is Sprotty's central nervous system, responsible for routing actions to their respective handlers and managing the flow of commands and responses throughout the application. + +**Key Responsibilities:** + +- Map actions to their respective handlers +- Dispatch actions and handles async request/response patterns + + +## Command Stack + +The **Command Stack** is the core state management that handles all model modifications through a command pattern with undo/redo capabilities. It bridges the actions and the actual model changes. + +**Key Responsibilities:** + +- Manages command execution (asynchronously) +- Manages undo/redo stacks +- Coordinate model updates with the Viewer + +## Viewer + +The **Viewer** is the rendering engine in Sprotty that transforms the internal model into visual representations using a virtual DOM (VDOM) approach. It manages the conversion from model elements to SVG elements. + +**Key Responsibilities:** + +- Map model elements to their representation as SVG elements +- Applies pre and post-postprocessing to virtual nodes +- Render individual model elements and their children recursively + +## Next Steps + +Now that you understand the core components, explore how they work together in the [Data Flow]({{< relref "data-flow" >}}) section to see how information moves through the system and how these components coordinate to provide a seamless diagramming experience. diff --git a/hugo/content/docs/concepts/data-flow.md b/hugo/content/docs/concepts/data-flow.md new file mode 100644 index 0000000..a4c5c33 --- /dev/null +++ b/hugo/content/docs/concepts/data-flow.md @@ -0,0 +1,32 @@ +--- +title: 'Data Flow' +weight: 3 +--- + +{{< toc >}} + +Now that you understand the [Core Components]({{< relref "core-components" >}}) of Sprotty's architecture, let's explore how data flows through the system. The data flow in Sprotty follows a unidirectional pattern, where all changes are initiated by actions and processed through a well-defined pipeline. + +## Unilateral Data Flow + +Here is how the unilateral data flow goes: + +- An event occur. For example the application request the data model, or a user selects a node in the diagram +- The **Model Source** react to this event and may send the corresponding **Action** to the **Action Dispatcher** +- The **Action Dispatcher** looks up the corresponding handler for the **Action** +- The **Action** may be converted to a **Command** and forwarded to the **Command Stack** +- The **Command Stack** executes the **Command**, resulting in a model update and notifying the **Viewer** +- The **Viewer** sends the new model to the DOM + +{{< mermaid class="text-center">}} +flowchart LR + ExternalEvent --> ModelSource + ModelSource -->|Create Action| ActionDispatcher + ActionDispatcher -->|Create Command| CommandStack + CommandStack -->|Update Model| Viewer + Viewer -->|Update DOM| DOM +{{< /mermaid>}} + +## Next Steps + +Understanding data flow is essential for building robust diagramming applications. In the next section, [Extension Points]({{< relref "extension-points" >}}), you'll learn how to customize and extend Sprotty's functionality by working with these data flow patterns. diff --git a/hugo/content/docs/concepts/extension-points.md b/hugo/content/docs/concepts/extension-points.md new file mode 100644 index 0000000..2e24f70 --- /dev/null +++ b/hugo/content/docs/concepts/extension-points.md @@ -0,0 +1,748 @@ +--- +title: 'Extension Points' +weight: 4 +--- + +{{< toc >}} + +Sprotty is designed to be highly extensible, allowing you to customize and enhance its functionality to meet your specific requirements. Sprotty's architecture is built around several key extension points that allow you to: + +- **Customize the rendering** of diagram elements +- **Add new behaviors** through actions and commands +- **Integrate with external data sources** +- **Modify the rendering pipeline** with post-processors +- All configurable through the **dependency injection** system + +Each extension point follows Sprotty's architectural principles: + +- **Separation of concerns**: Each extension has a specific responsibility +- **Dependency injection**: Extensions are loosely coupled and easily testable +- **Type safety**: All extensions are fully typed +- **Composability**: Multiple extensions can work together seamlessly + +## Dependency Injection + +Sprotty uses [InversifyJS](https://inversify.io/) for dependency injection, which provides a powerful and flexible way to configure and extend the framework. + +### Understanding the DI Container + +The DI container manages all the services and components in Sprotty. Each module registers its dependencies, and the container resolves them at runtime. + +```typescript +import { Container, ContainerModule } from 'inversify'; +import { TYPES, loadDefaultModules } from 'sprotty'; + +// Create a custom module +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + // Register your custom services here + bind(TYPES.IModelFactory).to(CustomModelFactory).inSingletonScope(); + bind(TYPES.IActionDispatcher).to(CustomActionDispatcher).inSingletonScope(); +}); + +// Create and configure the container +const container = new Container(); +loadDefaultModules(container); // Load Sprotty's default modules +container.load(customModule); // Load your custom module +``` + +### Key DI Concepts + +**Binding Types:** + +- `bind()`: Register a new service +- `rebind()`: Override an existing service +- `unbind()`: Remove a service binding +- `isBound()`: Check if a service is already bound + +**Scopes:** + +- `inSingletonScope()`: Single instance shared across the container +- `inTransientScope()`: New instance created each time +- `inRequestScope()`: Single instance per request + +### Common Extension Patterns + +**Service Replacement:** + +```typescript +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + // Replace the default logger with a custom one + rebind(TYPES.ILogger).to(CustomLogger).inSingletonScope(); + + // Replace the default model factory + rebind(TYPES.IModelFactory).to(CustomModelFactory).inSingletonScope(); +}); +``` + +**Service Addition:** + +```typescript +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + // Add a new service + bind(TYPES.ICustomService).to(CustomService).inSingletonScope(); + + // Register multiple implementations of the same interface + bind(TYPES.IActionHandler).to(CustomActionHandler1); + bind(TYPES.IActionHandler).to(CustomActionHandler2); +}); +``` + +**Conditional Binding:** + +```typescript +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + if (process.env.NODE_ENV === 'development') { + bind(TYPES.ILogger).to(VerboseLogger).inSingletonScope(); + } else { + bind(TYPES.ILogger).to(ProductionLogger).inSingletonScope(); + } +}); +``` + +## Custom Model Elements and Views + +Sprotty's rendering system is based on a model-view architecture where model elements define the data structure and views define how they are rendered. + +### Creating Custom Model Elements + +Model elements extend `SModelElementImpl` and define the data structure for your diagram elements. + +```typescript +import { SModelElementImpl, SShapeElementImpl } from 'sprotty'; + +export class CustomNode extends SShapeElementImpl { + customProperty: string = ''; + size: { width: number; height: number } = { width: 100, height: 50 }; + + constructor() { + super(); + this.type = 'custom-node'; + } +} + +export class CustomEdge extends SEdgeImpl { + customRouting: string = 'straight'; + + constructor() { + super(); + this.type = 'custom-edge'; + } +} +``` + +### Creating Custom Views + +Views define how model elements are rendered. Sprotty uses JSX with a custom `svg` function for SVG rendering. + +**SVG View Example:** + +```typescript +/** @jsx svg */ +import { svg } from 'sprotty/lib/jsx'; +import { injectable } from 'inversify'; +import { VNode } from 'snabbdom'; +import { SShapeElementView, IViewArgs, RenderingContext } from 'sprotty'; +import { CustomNode } from './model'; + +@injectable() +export class CustomNodeView extends SShapeElementView { + render(element: CustomNode, context: RenderingContext, args?: IViewArgs): VNode | undefined { + if (!this.isVisible(element, context)) { + return undefined; + } + + return + + + {element.customProperty} + + {context.renderChildren(element)} + ; + } +} +``` + +### Registering Model Elements and Views + +Use `configureModelElement` to register your custom model elements and views: + +```typescript +import { configureModelElement, TYPES } from 'sprotty'; + +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + // Register custom node + configureModelElement(context, 'custom-node', CustomNode, CustomNodeView); + + // Register custom edge + configureModelElement(context, 'custom-edge', CustomEdge, CustomEdgeView); + + // Register with features + configureModelElement(context, 'interactive-node', CustomNode, CustomNodeView, { + enable: [selectFeature, moveFeature, hoverFeature] + }); +}); +``` + +## Custom Actions and Commands + +Actions and commands are the primary mechanism for implementing custom behaviors in Sprotty. + +### Understanding Actions and Commands + +- **Actions**: Describe what should happen (intent) +- **Commands**: Implement how it should happen (execution) + +Actions are serializable objects that can be sent between client and server, while commands perform the actual state changes. + +### Creating Custom Actions + +Actions extend the base `Action` interface and define the data needed for the operation. + +```typescript +import { Action } from 'sprotty-protocol'; + +export interface CreateCustomNodeAction extends Action { + kind: 'createCustomNode'; + nodeType: string; + position: { x: number; y: number }; + parentId?: string; + properties?: Record; +} + +export interface UpdateCustomNodeAction extends Action { + kind: 'updateCustomNode'; + nodeId: string; + properties: Record; +} + +export interface DeleteCustomNodeAction extends Action { + kind: 'deleteCustomNode'; + nodeId: string; +} + +export interface MoveCustomNodeAction extends Action { + kind: 'moveCustomNode'; + nodeId: string; + newPosition: { x: number; y: number }; +} +``` + +### Creating Custom Commands + +Commands implement the actual behavior and must support undo/redo operations. + +**Simple Command:** + +```typescript +import { injectable, inject } from 'inversify'; +import { Command, CommandExecutionContext, CommandResult, TYPES } from 'sprotty'; +import { CreateCustomNodeAction } from './actions'; + +@injectable() +export class CreateCustomNodeCommand extends Command { + static KIND = 'createCustomNode'; + + private createdNodeId: string; + + constructor(@inject(TYPES.Action) protected readonly action: CreateCustomNodeAction) { + super(); + } + + execute(context: CommandExecutionContext): CommandResult { + const newNode = this.createNode(context); + const parent = this.action.parentId ? context.root.index.getById(this.action.parentId) : context.root; + + if (parent && 'children' in parent) { + parent.children.push(newNode); + } + + this.createdNodeId = newNode.id; + return CommandResult.ok(); + } + + undo(context: CommandExecutionContext): CommandResult { + const node = context.root.index.getById(this.createdNodeId); + if (node && node.parent && 'children' in node.parent) { + const index = node.parent.children.indexOf(node); + if (index >= 0) { + node.parent.children.splice(index, 1); + } + } + return CommandResult.ok(); + } + + redo(context: CommandExecutionContext): CommandResult { + return this.execute(context); + } + + private createNode(context: CommandExecutionContext): CustomNode { + const node = new CustomNode(); + node.id = context.modelFactory.createId(); + node.position = { ...this.action.position }; + node.type = this.action.nodeType; + + if (this.action.properties) { + Object.assign(node, this.action.properties); + } + + return node; + } +} +``` + +**Mergeable Command:** + +```typescript +import { injectable, inject } from 'inversify'; +import { MergeableCommand, CommandExecutionContext, CommandResult, TYPES } from 'sprotty'; +import { MoveCustomNodeAction } from './actions'; + +@injectable() +export class MoveCustomNodeCommand extends MergeableCommand { + static KIND = 'moveCustomNode'; + + private previousPosition?: { x: number; y: number }; + + constructor(@inject(TYPES.Action) protected readonly action: MoveCustomNodeAction) { + super(); + } + + execute(context: CommandExecutionContext): CommandResult { + const node = context.root.index.getById(this.action.nodeId) as CustomNode; + if (node) { + this.previousPosition = { ...node.position }; + node.position = { ...this.action.newPosition }; + } + return CommandResult.ok(); + } + + undo(context: CommandExecutionContext): CommandResult { + const node = context.root.index.getById(this.action.nodeId) as CustomNode; + if (node && this.previousPosition) { + node.position = { ...this.previousPosition }; + } + return CommandResult.ok(); + } + + redo(context: CommandExecutionContext): CommandResult { + return this.execute(context); + } + + merge(other: Command): boolean { + if (other instanceof MoveCustomNodeCommand && other.action.nodeId === this.action.nodeId) { + this.action.newPosition = { ...other.action.newPosition }; + return true; + } + return false; + } +} +``` + +### Creating Action Handlers + +Action handlers connect actions to commands and can perform additional logic. + +```typescript +import { injectable } from 'inversify'; +import { IActionHandler, ICommand } from 'sprotty'; +import { CreateCustomNodeAction, UpdateCustomNodeAction, DeleteCustomNodeAction } from './actions'; + +@injectable() +export class CustomActionHandler implements IActionHandler { + handle(action: Action): ICommand | Action | void { + switch (action.kind) { + case 'createCustomNode': + return new CreateCustomNodeCommand( + action.nodeType, + action.position, + action.parentId, + action.properties + ); + + case 'updateCustomNode': + return new UpdateCustomNodeCommand( + action.nodeId, + action.properties + ); + + case 'deleteCustomNode': + return new DeleteCustomNodeCommand(action.nodeId); + } + } +} +``` + +### Registering Actions and Commands + +Register your action handlers and commands in the DI container: + +```typescript +import { TYPES, configureCommand } from 'sprotty'; + +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + // Register action handler + bind(TYPES.IActionHandler).to(CustomActionHandler); + + // Register commands using configureCommand + configureCommand(context, CreateCustomNodeCommand); + configureCommand(context, UpdateCustomNodeCommand); + configureCommand(context, DeleteCustomNodeCommand); +}); +``` + +## Custom Model Sources + +Model sources are responsible for providing the diagram data to Sprotty. They can connect to various data sources and handle the communication between the client and external systems. + +### Understanding Model Sources + +Model sources implement the `ModelSource` interface and serve as the entry point for external data. They handle: + +- Model requests from the client +- Model updates and synchronization +- Communication with external systems +- Bounds computation coordination (when `needsClientLayout` is enabled) +- Action handling and forwarding (for selected action types) + +### Creating a Local Model Source + +For simple applications, you can extend `LocalModelSource` to provide static or dynamic data. + +```typescript +import { injectable } from 'inversify'; +import { LocalModelSource, SModelRootImpl } from 'sprotty'; + +@injectable() +export class CustomLocalModelSource extends LocalModelSource { + private currentModel: SModelRootImpl; + + constructor() { + super(); + this.currentModel = this.createInitialModel(); + } + + get model(): SModelRootImpl { + return this.currentModel; + } + + async updateModel(newModel: SModelRootImpl): Promise { + this.currentModel = newModel; + await this.updateRoot(); + } + + private createInitialModel(): SModelRootImpl { + const root = new SModelRootImpl(); + root.id = 'root'; + root.type = 'graph'; + + // Add your initial model elements here + const node = new CustomNode(); + node.id = 'node1'; + node.position = { x: 100, y: 100 }; + root.children.push(node); + + return root; + } +} +``` + +### Creating a Remote Model Source + +For applications that need to communicate with a server, extend `DiagramServerProxy`. + +```typescript +import { injectable } from 'inversify'; +import { DiagramServerProxy, SModelRootImpl } from 'sprotty'; + +@injectable() +export class CustomDiagramServer extends DiagramServerProxy { + constructor() { + super(); + } + + protected async requestModel(): Promise { + // Implement your server communication logic + const response = await fetch('/api/diagram/model'); + const modelData = await response.json(); + return this.createModelFromData(modelData); + } + + protected async handleAction(action: Action): Promise { + // Handle actions that need server processing + if (action.kind === 'createCustomNode') { + await this.sendToServer(action); + } + } + + private async sendToServer(action: Action): Promise { + await fetch('/api/diagram/action', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(action) + }); + } + + private createModelFromData(data: any): SModelRootImpl { + // Convert your data format to Sprotty model + const root = new SModelRootImpl(); + root.id = data.id; + root.type = data.type; + + // Convert nodes + for (const nodeData of data.nodes) { + const node = new CustomNode(); + node.id = nodeData.id; + node.position = nodeData.position; + node.customProperty = nodeData.customProperty; + root.children.push(node); + } + + return root; + } +} +``` + +### Creating a WebSocket Model Source + +For real-time applications, you can create a WebSocket-based model source. + +```typescript +import { injectable } from 'inversify'; +import { ModelSource, SModelRootImpl } from 'sprotty'; + +@injectable() +export class WebSocketModelSource extends ModelSource { + private ws: WebSocket; + private currentModel: SModelRootImpl; + + constructor() { + super(); + this.ws = new WebSocket('ws://localhost:8080/diagram'); + this.setupWebSocket(); + } + + get model(): SModelRootImpl { + return this.currentModel; + } + + handle(action: Action): ICommand | Action | void { + // Send actions to server via WebSocket + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(action)); + } + } + + async commitModel(newRoot: SModelRootImpl): Promise { + const previousModel = this.currentModel; + this.currentModel = newRoot; + return previousModel; + } + + private setupWebSocket(): void { + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.requestModel(); + }; + + this.ws.onmessage = (event) => { + const data = JSON.parse(event.data); + this.handleServerMessage(data); + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + this.ws.onclose = () => { + console.log('WebSocket disconnected'); + }; + } + + private handleServerMessage(data: any): void { + switch (data.type) { + case 'model': + this.currentModel = this.createModelFromData(data.model); + this.updateRoot(); + break; + case 'action': + this.actionDispatcher.dispatch(data.action); + break; + } + } + + private createModelFromData(data: any): SModelRootImpl { + // Implementation similar to previous example + } +} +``` + +## Custom Post-processors + +Post-processors allow you to modify the rendered VNodes after they are created but before they are applied to the DOM. This is useful for adding custom styling, event handlers, or other modifications. + +### Understanding Post-processors + +Post-processors implement the `IVNodePostprocessor` interface and are called for each VNode during the rendering process. They can: + +- Modify VNode attributes and properties +- Add event listeners +- Apply custom styling +- Add animations +- Perform DOM manipulations + +### Creating a Simple Post-processor + +```typescript +import { injectable } from 'inversify'; +import { VNode } from 'snabbdom'; +import { IVNodePostprocessor, SModelElementImpl } from 'sprotty'; +import { setAttr, setClass } from 'sprotty/lib/base/views/vnode-utils'; + +@injectable() +export class CustomStylingPostprocessor implements IVNodePostprocessor { + decorate(vnode: VNode, element: SModelElementImpl): VNode { + // Add custom CSS class based on element type + if (element.type === 'custom-node') { + setClass(vnode, 'custom-node-style', true); + } + + // Add custom attributes + if (element.id.includes('important')) { + setAttr(vnode, 'data-important', 'true'); + } + + return vnode; + } + + postUpdate(): void { + // Called after all VNodes are updated + // Use this for cleanup or global updates + } +} +``` + +### Creating an Event Handler Post-processor + +```typescript +import { injectable } from 'inversify'; +import { VNode } from 'snabbdom'; +import { IVNodePostprocessor, SModelElementImpl } from 'sprotty'; +import { on } from 'sprotty/lib/base/views/vnode-utils'; + +@injectable() +export class CustomEventHandlerPostprocessor implements IVNodePostprocessor { + decorate(vnode: VNode, element: SModelElementImpl): VNode { + // Add custom click handler + if (element.type === 'custom-node') { + on(vnode, 'click', (event: Event) => { + event.preventDefault(); + this.handleCustomClick(element, event); + }); + } + + // Add custom hover handlers + on(vnode, 'mouseenter', (event: Event) => { + this.handleMouseEnter(element, event); + }); + + on(vnode, 'mouseleave', (event: Event) => { + this.handleMouseLeave(element, event); + }); + + return vnode; + } + + postUpdate(): void { + // Cleanup if needed + } + + private handleCustomClick(element: SModelElementImpl, event: Event): void { + console.log('Custom click on element:', element.id); + // Dispatch custom action or perform other logic + } + + private handleMouseEnter(element: SModelElementImpl, event: Event): void { + // Add hover effect + } + + private handleMouseLeave(element: SModelElementImpl, event: Event): void { + // Remove hover effect + } +} +``` + +### Creating an Animation Post-processor + +```typescript +import { injectable } from 'inversify'; +import { VNode } from 'snabbdom'; +import { IVNodePostprocessor, SModelElementImpl } from 'sprotty'; +import { setAttr } from 'sprotty/lib/base/views/vnode-utils'; + +@injectable() +export class AnimationPostprocessor implements IVNodePostprocessor { + private animatedElements = new Set(); + + decorate(vnode: VNode, element: SModelElementImpl): VNode { + // Add animation attributes for new elements + if (!this.animatedElements.has(element.id)) { + setAttr(vnode, 'class', 'fade-in'); + setAttr(vnode, 'style', { + animation: 'fadeIn 0.5s ease-in-out' + }); + this.animatedElements.add(element.id); + } + + // Add transition for position changes + if (element.position) { + setAttr(vnode, 'style', { + transition: 'transform 0.3s ease-in-out' + }); + } + + return vnode; + } + + postUpdate(): void { + // Clean up animation tracking if needed + } +} +``` + +### Registering Post-processors + +Register your post-processors in the DI container: + +```typescript +import { TYPES } from 'sprotty'; + +const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { + // Register post-processors + bind(CustomStylingPostprocessor).toSelf().inSingletonScope(); + bind(TYPES.IVNodePostprocessor).toService(CustomStylingPostprocessor); + + bind(CustomEventHandlerPostprocessor).toSelf().inSingletonScope(); + bind(TYPES.IVNodePostprocessor).toService(CustomEventHandlerPostprocessor); + + bind(AnimationPostprocessor).toSelf().inSingletonScope(); + bind(TYPES.IVNodePostprocessor).toService(AnimationPostprocessor); +}); +``` + +By following these extension points and best practices, you can create powerful, maintainable, and performant customizations for your Sprotty-based diagramming applications. diff --git a/hugo/content/docs/recipes/architecture-overview.md b/hugo/content/docs/recipes/architecture-overview.md deleted file mode 100644 index 0cc5035..0000000 --- a/hugo/content/docs/recipes/architecture-overview.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: 'Architecture Overview' ---- - -{{< toc >}} - -The base architecture of Sprotty revolves around an unidirectional cyclic flow of information between three major components: the [`ActionDispatcher`](#action-dispatcher), the [`CommandStack`](#command-stack), and the [`Viewer`](#viewer). This leads to a clear and easily testable flow of data which prevents feedback loops. - -{{< mermaid class="text-center">}} -flowchart TD; -ActionDispatcher -CommandStack -Viewer -ActionDispatcher -->|Command| CommandStack -CommandStack -->|SModel| Viewer -Viewer -->|Action| ActionDispatcher -{{< /mermaid>}} - -## Action Dispatcher - -The main role of the `ActionDispatcher` is to receive an [`Action`](#action) and produce a corresponding command to be transmitted to the [`CommandStack`](#command-stack) using `ActionHandlers`. All operations on the diagram must be passed through the `ActionDispatcher`, so the [`CommandStack`](#command-stack) and the [`Viewer`](#viewer) must never be invoked directly. - - The `ActionDispatcher` also communicates with the [`ModelSource`](#model-source) through [`Action`](#action)s in a bidirectional manner, for example to inject external data into the loop or apply edits to the [`ModelSource`](#model-source). - -{{< mermaid class="text-center">}} -flowchart LR; -ModelSource -ActionDispatcher -ModelSource -->|Action| ActionDispatcher -ActionDispatcher -->|Action| ModelSource -{{< /mermaid>}} - -### Action - -`Action`s are objects without behavior, JSON structures that describe what should happen but not *how* it should happen. As such, they can be serialized and serve as protocol messages that are exchanged between the client and the server. In actions, model elements are referred to by their IDs. - -### Model Source - -There are two different `ModelSource`s: the `LocalModelSource` offers an API to control the model directly in the client, while the `DiagramServer` delegates to a remote source, e.g. through a WebSocket or a VSCode extension. - -## Command Stack - -The `CommandStack` executes the commands it receives from the [`ActionDispatcher`](#action-dispatcher). It chains the promises returned by the execution methods and keeps an undo and a redo stack. It merges the current commands with the last one, e.g. to only keep the start and end point of a move from a drag operation. It is responsible for producing a graph model (namely [`SModel`](#smodel-sprottymodel)) and forwards it to the [`Viewer`](#viewer) to be rendered. - -## Command - -`Command`s describe the behavior of their corresponding [`Action`](#action). They have the typical methods `execute()`, `undo()`and `redo()`, each of which take the current model and a command execution context as parameter, and return the new model or a promise for it. The latter serves to chain asynchronous commands such as animations. - -### SModel (SprottyModel) - -The diagram is stored in an internal model called `SModel`. The root of the diagram is always an instance of `SModelRootImpl` and holds an index of the model to allow fast lookup of elements by ID. All elements of a diagram inherit `SModelElementImpl` which has a unique ID and a mandatory type referring to its [`View`](#view). The model elements are organized in a tree derived from the `children` and `parent` properties of each model element. It can be useful to introduce domain-specific information into the `SModel`. This can be achieved via creating new element classes that inherit from any related `SModelElementImpl`. - -{{< mermaid class="text-center">}} -flowchart BT; -SModelElementImpl -SShapeElementImpl -SEdgeImpl -SNodeImpl -SPortImpl -SLabelImpl -CustomEdge -CustomNode -CustomPort -CustomLabel -SShapeElementImpl --> SModelElementImpl -SEdgeImpl --> SModelElementImpl -CustomEdge -.-> SEdgeImpl -SNodeImpl --> SShapeElementImpl -CustomNode -.-> SNodeImpl -SPortImpl --> SShapeElementImpl -CustomPort -.-> SPortImpl -SLabelImpl --> SShapeElementImpl -CustomLabel -.-> SLabelImpl -{{< /mermaid>}} - -## Viewer - -The `Viewer` is responsible for turning the internal model into its representation in the DOM. The conversion from an `SModel` to its representation in the DOM is not direct. Instead, Sprotty first creates a `VirtualDOM` and uses it to patch the actual DOM. This approach saves on expensive modification of the DOM by applying only the minimum amount of modification to it. -The `Viewer` receives an [`SModel`](#smodel-sprottymodel) from the [`CommandStack`](#command-stack) and traverses it to apply a corresponding [`View`](#view) to every element. -The viewer is also responsible to add event listeners and animations using its `Decorator`s. The received events should be converted to [`Action`](#action)s and transferred to the [`ActionDispatcher`](#action-dispatcher). - -{{< mermaid class="text-center">}} -flowchart LR; -Viewer -ViewRegistry -Views -VirtualDOM -DOM -Viewer --> ViewRegistry -ViewRegistry --> Views -Views -->|render| VirtualDOM -VirtualDOM -->|patch| DOM -DOM -->|event| Viewer -{{< /mermaid>}} - -### View Registry - -The [`Viewer`](#viewer) uses the `ViewRegistry` to look up the [`View`](#view) for a graph model element using its ID. - -### View - -A `View` knows how to turn a graph model element and its children into a virtual DOM node. It uses JSX technology and contains a `render` method producing one or a group of SVG elements. diff --git a/hugo/content/docs/recipes/custom-interactions.md b/hugo/content/docs/recipes/custom-interactions.md index 7a7caa1..42d29b2 100644 --- a/hugo/content/docs/recipes/custom-interactions.md +++ b/hugo/content/docs/recipes/custom-interactions.md @@ -26,7 +26,7 @@ const container = new ContainerModule((bind, unbind, isBound, rebind) => { ``` A button handler is a simple injectable class with a `buttonPressed(button: SButton): Action[]` method. -The actions that this method returns are passed to the [`ActionDispatcher`]({{< ref "/docs/recipes/architecture-overview" >}}#action-dispatcher) to be handled there. +The actions that this method returns are passed to the [`ActionDispatcher`]({{< ref "/docs/concepts/architecture-overview" >}}#action-dispatcher) to be handled there. ```Typescript @injectable() @@ -41,7 +41,7 @@ export class CustomButtonHandler implements IButtonHandler { ## Mouse and Keyboard Listeners Sprotty also offers the ability to attach mouse and keyboard listeners by registering `MouseListener` or `KeyListener`. -This can be simply done by binding the custom listener to the respective listener type in your [DI-container]({{< ref "/docs/recipes/dependency-injection" >}}) like this: +This can be simply done by binding the custom listener to the respective listener type in your [DI-container]({{< ref "/docs/concepts/extension-points">}}#dependency-injection) like this: ```Typescript bind(CustomMouseListener).toSelf().inSingletonScope(); diff --git a/hugo/content/docs/recipes/dependency-injection.md b/hugo/content/docs/recipes/dependency-injection.md deleted file mode 100644 index a294d0f..0000000 --- a/hugo/content/docs/recipes/dependency-injection.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: 'Sprotty configuration and dependency injection' ---- -{{< toc >}} -As seen in the [getting started]({{< ref "/docs/learn/getting-started" >}}) guide, Sprotty relies heavily on dependency injection (DI) through [InversifyJs](https://inversify.io/) for the configuration of its various components. This chapter will take a closer look at how to work with this. - -## Why dependency injection? - -DI allows us to: - -- not care about the instantiation and life-cycle of service components -- manage singletons like the various registries without using the global scope -- easily mock components in tests -- exchange default implementations with custom ones with minimum code changes -- modularize the configuration of specific features and scenarios and merge these modules for the final application - -## The container - -The DI-container is the main point of configuration. The standard in Sprotty is to name this file `di.config.ts`. - -```typescript -export const createContainer = (containerId: string) => { - const myModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); - const context = { bind, unbind, isBound, rebind }; - configureModelElement(context, 'graph', SGraphImpl, SGraphView); - configureModelElement(context, 'task', SNodeImpl, TaskNodeView); - configureModelElement(context, 'edge', SEdgeImpl, PolylineEdgeView); - configureViewerOptions(context, { - needsClientLayout: false, - baseDiv: containerId - }); - }); - const container = new Container(); - loadDefaultModules(container); - container.load(myModule, edgeIntersectionModule); - return container; -}; -``` - -The container is built from multiple modules. Through `loadDefaultModules()` all modules are loaded for default Sprotty functionalities. We can also load other optional modules like the `edgeIntersectionModule` for extra functionality. - -Most important is our own module where the core of the configuration happens. Here we can configure singleton scope classes like our [model source]({{< ref "/docs/recipes/model-sources" >}}) or rebind default Sprotty components (for example the logger) to a custom implementation. We use Symbols for bindings instead of using classes directly. All Symbols can be found in the `TYPES` object. - -Using `configureModelElement` we can link our model to specific view components through the type property. Meaning if we have the following SNode, -in our model, Sprotty will try to convert this data structure to an instance of the actual `SNodeImpl` class and render it with the `TaskNodeView`. - -```Typescript -{ - type: 'task', - id: 'task01', - name: 'First Task', - isFinished: true, - ... -} -``` - -Lastly, we need to configure our viewer options. Here we configure all the DOM elements needed by Sprotty, for example the base `div` inside of which our diagram is rendered, or the hidden `div` used by the first render cycle for determining micro layout. Another thing configured here is the layout. Specifically, if layout calculation should be done on client-side, server-side or both. This also determines the protocol spoken by the client and server. - -## Features - -Model elements can further be configured through [features]({{< ref "/docs/ref/features" >}}). - -```typescript -configureModelElement(context, 'task', SNodeImpl, TaskNodeView, { - enable: [customFeature], - disable: [moveFeature] -}); -``` - -the `configureModelElement` method takes an optional object as the last parameter containing arrays for `enabled` and `disabled` features, which in turn contain Symbols representing those features. Through this, we can disable default functionality like dragging or selecting nodes and add functionality, either custom or loaded from other non-default modules. - -## Creating custom components - -As described previously Sprotty uses InversifyJs for dependency injection. That means when creating custom features, views, etc. we have to use this too. -As an example, let's look at Sprotty's `PolylineEdgeView`. - -```Typescript -@injectable() -export class PolylineEdgeView extends RoutableView { - - @inject(EdgeRouterRegistry) edgeRouterRegistry: EdgeRouterRegistry; - ... -} -``` - -The most important thing for our component to be made available in Sprotty is annotating it with [`@injectable()`](https://github.com/inversify/InversifyJS/blob/master/src/annotation/injectable.ts). Otherwise the dependency injection won't work. - -Now, as seen in the example [above](#the-container), we can just bind it in the container like this: - -```Typescript -configureModelElement(context, 'edge', SEdge, PolylineEdgeView); -``` - -After that we can use all features of inversifyJs and inject other components registered in our container with [`@inject(...)`](https://github.com/inversify/InversifyJS/blob/master/src/annotation/inject.ts) - -For more information on inversifyJs have a look to their [documentation](https://github.com/inversify/InversifyJS/blob/master/wiki/readme.md) - -## Dependency Injection specialties - -### Multi bindings - -Sometimes there is more than one implementation bound to a specific interface in Sprotty. This is when we use multi-bindings. Here is an example of the `VNodeDecorator`. - -```Typescript -@multiInject(TYPES.VNodePostprocessor)@optional() protected postprocessors: VNodePostprocessor[] -``` - -### Provider Bindings - -Sprotty's circular event flow introduces a cyclic dependency between the components `ActionDispatcher`, `CommandStack` and `Viewer`. To handle these, we have to use provider bindings like this: - -```Typescript -// action-dispatcher.ts -export type IActionDispatcherProvider = () => Promise; -``` - -```Typescript -// di.config.ts -bind(TYPES.IActionDispatcher).to(ActionDispatcher).inSingletonScope(); -bind(TYPES.IActionDispatcherProvider).toProvider(ctx => { - return () => { - return new Promise((resolve) => { - resolve(ctx.container.get(TYPES.IActionDispatcher)); - }); - }; -}); -``` diff --git a/hugo/content/docs/recipes/micro-layout.md b/hugo/content/docs/recipes/micro-layout.md index f8413fe..92f839f 100644 --- a/hugo/content/docs/recipes/micro-layout.md +++ b/hugo/content/docs/recipes/micro-layout.md @@ -12,7 +12,7 @@ Any model element that implements or extends the `SNode` or `SCompartment` inter * `hbox`: children elements are arranged horizontally * `vbox`: children elements are arranged vertically -The `layout` property aims at arranging children elements that do not have a meaning in terms of graph hierarchy (i.e. labels, buttons, ...). Please note that children [that are instances of `SNodeImpl`]({{< ref "/docs/recipes/dependency-injection" >}}#the-container) do not respect the `layout` property by default (more on that [later](#layouting-nested-nodes)). +The `layout` property aims at arranging children elements that do not have a meaning in terms of graph hierarchy (i.e. labels, buttons, ...). Please note that children [that are instances of `SNodeImpl`]({{< ref "/docs/concepts/extension-points">}}#dependency-injection) do not respect the `layout` property by default (more on that [later](#layouting-nested-nodes)). First and foremost, the micro-layout engine needs to be activated in the inversify container. This is done by setting the `needsClientLayout` property to `true` in the inversify container configuration: @@ -115,7 +115,7 @@ This results in the following visuals: ![layout-configuration](/assets/docs/layout-configuration.png) -If you want different layout configurations, you can implement your own micro-layout engine and inject it via [Dependency Injection]({{< ref "/docs/recipes/dependency-injection" >}}). +If you want different layout configurations, you can implement your own micro-layout engine and inject it via [Dependency Injection]({{< ref "/docs/concepts/extension-points">}}#dependency-injection). ## Layout Options diff --git a/hugo/content/docs/recipes/model-sources.md b/hugo/content/docs/recipes/model-sources.md deleted file mode 100644 index 2511f0d..0000000 --- a/hugo/content/docs/recipes/model-sources.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: 'Model Sources' ---- -{{< toc >}} -When drawing a diagram with Sprotty we need a place to define and update the schema of the diagram to draw. Sprotty uses *model sources* to do this. -Sprotty currently offers two different model sources: The `LocalModelSource` for local models and the `DiagramServer` for remote ones. -{{< mermaid class="text-center">}} -flowchart TD; -ActionDispatcher -ModelSource -LocalModelSource -DiagramServerProxy -DiagramServer -ActionDispatcher <-.->|Action| ModelSource -ModelSource --- LocalModelSource -ModelSource --- DiagramServerProxy -DiagramServerProxy <-.->|Action| DiagramServer -{{< /mermaid>}} -Regardless of where your model-source is located, Sprotty handles them in a similar fashion. All communication between `ActionDispatcher` and model source is always through [actions]({{< ref "/docs/recipes/actions-and-protocols" >}}) and is bi-directional. -This is a powerful feature of Sprotty as it allows both flexibility regarding where and how the Diagram is generated, as well as changing or updating and reacting to interactions with the diagram simultaneously. - -The following sections will explain how to use and work with the different types of model sources. - -## General usage - -Regardless of the model source we are using, the first thing we have to do is to register our model source in the front-end [DI-container]({{< ref "/docs/recipes/dependency-injection" >}}) like this: - -```Typescript -bind(TYPES.ModelSource).to(ModelSourceClassOrProxy).inSingletonScope(); -``` - -After that, we can retrieve the model source with the following code to further configure and use it. - -```Typescript -const modelSource = container.get(TYPES.ModelSource); -``` - -## Local Model Source - -A `LocalModelSource` instance allows us to set and modify the model through function calls, and keeps the model schema saved locally. -To see how to use this model source, let's have a look at the following example: - -```Typescript -import {SNode} from 'sprotty-protocol'; -export default runExample() { - const container = createContainer('sprotty-showcase'); - const modelSource = container.get(TYPES.ModelSource); - modelSource.setModel({ - type: 'graph' - children: [ - { - type: 'node', - id: 'main_node', - text: 'node1', - position: {x: 0, y: 0} - } - ] - }); - - document.getElementById('addButton').addEventListener('click', () => { - modelSource.addElements([ - { - parentId: 'graph', - element: { - type: 'node', - id: 'new_node', - text: 'new node', - position: {x: 100, y: 100} - } - }]) - }) -} -``` - -In this example, we have a hard-coded data structure containing all the initial elements which are set in the model. -Be aware that by defining the model this way we are defining the *model schema* - a data structure describing the model - and not the actual *model* itself. -The *model* in this context means instances of the classes containing logic Sprotty uses for rendering. The *model schema* is used by Sprotty to generate the *model*. -Due to this, we should use the interfaces for our nodes, edges, etc. from `sprotty-protocol` and not the classes from the `sprotty` main package. -For disambiguation, model classes are suffixed with `Impl` in Sprotty, while interfaces (contained in the package `sprotty-protocol`) are not. This means that `SNode` is an interface, while `SNodeImpl` is a class. - -After defining the *model schema* we can then use methods like `addElements()` from our `LocalModelSource` to add new nodes at the click of a button. The `LocalModelSource` then handles updating the model and notifying the `ActionDispatcher` about the update, so that the view can receive an animated update. - -Through methods like the ones outlined above, the `LocalModelSource` can also be used as a facade over the action-based API of Sprotty. It handles actions for bounds calculation and model updates. - -## Diagram Server - -When the model needs to be generated from a remote source, like in a worker or from a server, we can use Sprotty's `DiagramServer` model source. It communicates with the client through `Action` objects which can be serialized to plain JSON. - -On the client-side, instead of registering an actual `ModelSource` we can use a `DiagramServerProxy`. The Proxy handles the communication and forwards actions to the `ActionDispatcher`. Out of the box, Sprotty offers the `WebSocketDiagramServerProxy` for communicating through WebSockets with the `DiagramServer`. -Should a different form of communication be necessary we would have to [create a custom proxy](#creating-a-custom-model-source-proxy). - -Using the `WebSocketDiagramServerProxy` is quite simple. We just need to call `listen` on the ModelSource and pass it the WebSocket we're communicating with. - -```Typescript -const modelSource = container.get(TYPES.ModelSource); -modelSource.listen(websocket); -``` - -For creating the `DiagramServer` itself, let's look at an example. - -```Typescript -// Creating a new websocket server -const wss = new WebSocketServer.Server({ port: 8080 }); - -// create our DiagramServices -const elkFactory: ElkFactory = () => new SocketElkServer(); -const services: DiagramServices = { - DiagramGenerator: new RandomGraphGenerator(), - ModelLayoutEngine: new ElkLayoutEngine(elkFactory) -} - -// Creating connection using websocket -wss.on("connection", ws => { - - const diagramServer = new DiagramServer(action => { - ws.send(JSON.stringify(action)); - }, services) - - ws.on('message' data => { - diagramServer.accept(data.action); - }); -}); -``` - -In the example above, we assume we have a simple [nodeJs WebSocket server](https://github.com/websockets/ws) and want to create a `DiagramServer` for it. - -As we can see, there are two parts to creating the `DiagramServer`. -First, we need a dispatch method to send actions from the server to the client. This can be as simple as calling `ws.send()` with the serialized action. -Second, we need the `DiagramServices`. The `DiagramServices` type looks like this: - -```Typescript -export interface DiagramServices { - readonly DiagramGenerator: IDiagramGenerator - readonly ModelLayoutEngine?: IModelLayoutEngine - readonly ServerActionHandlerRegistry?: ServerActionHandlerRegistry -} -``` - -There are 3 components to the `DiagramServices`. One is mandatory, the other two are optional: - -- The `DiagramGenerator` which the server uses to create the schema of the Diagram -- Optionally the `ModelLayoutEngine`, like the `ElkLayoutEngine` from [sprotty-elk](https://github.com/eclipse-sprotty/sprotty/tree/master/packages/sprotty-elk), if we want to do server-side layouting -- Optionally the `ServerActionHandlerRegistry` for overwriting the default handling of incoming actions - -## Creating a Custom Model Source Proxy - -In case communication between the `DiagramServer` and client does not work through WebSockets, for example when the `DiagramServer` is running in a worker or the sprotty client is in a vscode webview (see [sprotty-vscode](https://github.com/eclipse-sprotty/sprotty-vscode)), we can easily implement our own proxy instead. - -```Typescript -export class WebWorkerDiagramProxy extends DiagramServerProxy { - constructor(private worker: Worker) { - super() - const proxy = this; - worker.onmessage = function(event) { - proxy.messageReceived(event.data) - } - - } - - protected sendMessage(message: ActionMessage): void { - this.worker.postMessage(JSON.stringify(message)); - } -} -``` - -Following the example above, first we need to extend `DiagramServerProxy`. This already gives us most of our needed functionality and makes this proxy a `ModelSource`. -Then we need to listen for incoming messages and pass them to the `messageReceived()` function, which deserializes and passes them to the `ActionDispatcher`. -Lastly, we need to implement the `sendMessage()` method to allow actions coming from the `ActionDispatcher` to be transferred to the `DiagramServer`. - -Now our custom model source proxy is able to propagate all actions between the `ActionDispatcher` and our `DiagramServer` running in the worker, which gives us access to all of sprotty's functionality. diff --git a/hugo/content/docs/recipes/svg-rendering.md b/hugo/content/docs/recipes/svg-rendering.md index 966b5d4..b1d39a7 100644 --- a/hugo/content/docs/recipes/svg-rendering.md +++ b/hugo/content/docs/recipes/svg-rendering.md @@ -33,7 +33,7 @@ export class NodeView extends RectangularNodeView { } ``` -The class `NodeView` extends `RectangularNodeView` which is a default `View` in Sprotty, ultimately implementing `IView`. Don't forget to add the class decorator `@injectable()`, which is necessary for the [Dependency Injection]({{< ref "/docs/recipes/dependency-injection" >}}) mechanism. +The class `NodeView` extends `RectangularNodeView` which is a default `View` in Sprotty, ultimately implementing `IView`. Don't forget to add the class decorator `@injectable()`, which is necessary for the [Dependency Injection]({{< ref "/docs/concepts/extension-points">}}#dependency-injection). The `render()` method is the core of the `View`. It takes `node` -- that is the model element to be rendered - as an argument, a `RenderingContext`, and an optional `args` object. View implementations should first check whether the `node` should be rendered at all. This is an optimization step, as we only want to render SVG elements that are inside of the visible viewport and not hidden by some other user-defined filter. Eventually, the `render()` method returns a `VNode` which is [Snabbdom's](https://github.com/snabbdom/snabbdom) virtual representation of a DOM element. This `VNode` can hold one and only one *root element*, therefore we need to group our SVG elements inside of a *container element* `g`. diff --git a/hugo/content/docs/ref/features.md b/hugo/content/docs/ref/features.md index 1a6d761..ffd0a8a 100644 --- a/hugo/content/docs/ref/features.md +++ b/hugo/content/docs/ref/features.md @@ -17,7 +17,7 @@ export class SNodeImpl extends SConnectableElementImpl implements Selectable, Fa } ``` -It is possible to fine-tune the behavior in the [dependency injection container]({{< ref "/docs/recipes/dependency-injection" >}}#the-container) by enabling or disabling features for a given model element type. +It is possible to fine-tune the behavior in the [dependency injection container]({{< ref "/docs/concepts/extension-points">}}#dependency-injection) by enabling or disabling features for a given model element type. ```typescript configureElement('my-node-type', SNodeImpl, RectangularNodeView, {enable: [layoutableChildFeature], disable: [moveFeature]}) diff --git a/hugo/content/docs/ref/smodel.md b/hugo/content/docs/ref/smodel.md index 415ecce..a0be946 100644 --- a/hugo/content/docs/ref/smodel.md +++ b/hugo/content/docs/ref/smodel.md @@ -64,7 +64,7 @@ This is the base class for **all** elements of the diagram model. This ensures t * `type: string`: The type of the element. This value is used in the Sprotty configuration to specify the corresponding view for all elements of this type. * `id: string`: The globally unique identifier of the element. -* `features: FeatureSet` - *optional*: A set of [features]({{< ref "/docs/ref/features" >}}) that are enabled on the element. The list of features can be further configured in the [dependency injection container]({{< ref "/docs/recipes/dependency-injection" >}}#features). +* `features: FeatureSet` - *optional*: A set of [features]({{< ref "/docs/ref/features" >}}) that are enabled on the element. The list of features can be further configured in the [dependency injection container]({{< ref "/docs/concepts/extension-points">}}#dependency-injection). * `cssClasses: string[]` - *optional*: A list of CSS classes that should be applied to the element. ### SParentElementImpl diff --git a/hugo/data/menu/main.yaml b/hugo/data/menu/main.yaml index 6d5c3e4..84367ab 100644 --- a/hugo/data/menu/main.yaml +++ b/hugo/data/menu/main.yaml @@ -15,20 +15,26 @@ main: ref: "docs/learn/dependency-injection" - name: Putting It Together ref: "docs/learn/putting-it-together" - - name: Recipes + - name: Concepts sub: - name: Architecture Overview - ref: "docs/recipes/architecture-overview" - - name: Sprotty Configuration and Dependency Injection - ref: "docs/recipes/dependency-injection" + ref: "docs/concepts/architecture-overview" + - name: Core Components + ref: "docs/concepts/core-components" + - name: Data Flow + ref: "docs/concepts/data-flow" + - name: Extension Points + ref: "docs/concepts/extension-points" + - name: Best Practices + ref: "docs/concepts/best-practices" + - name: Recipes + sub: - name: SVG Rendering ref: "docs/recipes/svg-rendering" - name: Micro-layout ref: "docs/recipes/micro-layout" - name: Styling ref: "docs/recipes/styling" - - name: Model Sources - ref: "docs/recipes/model-sources" - name: Communication and Protocols ref: "docs/recipes/actions-and-protocols" - name: Creating Custom Interactions From 0933fc49bc618332b649e731031e382de5f213c2 Mon Sep 17 00:00:00 2001 From: Jan Bicker Date: Tue, 29 Jul 2025 13:33:55 +0000 Subject: [PATCH 2/5] changed version of upload-pages-artifact action --- .github/workflows/deploy.yml | 8 ++++---- .github/workflows/preview.yml | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9da6f70..5f71b83 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,14 +29,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '18' - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Pages id: pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v5 - name: Build env: # For maximum backward compatibility with Hugo modules @@ -46,7 +46,7 @@ jobs: npm install npm run build - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v3.0.1 with: path: ./public diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index b13e442..8a13645 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -20,11 +20,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Node.js - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 + uses: actions/setup-node@v4 with: node-version: '18' - name: Checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Build @@ -32,7 +32,7 @@ jobs: npm install npm run build - name: Upload artifact - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@v4 with: name: 'site' path: ./public @@ -49,13 +49,13 @@ jobs: # checkout required for pr-preview-action to succeed, # while the content will not be used - name: Checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@v4 - name: Download the preview page - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@v4 with: name: 'site' path: ./public - - uses: rossjrw/pr-preview-action@4668d7cb417ce7067b0b59bc152b1ae1513010de # v1.4.6 + - uses: rossjrw/pr-preview-action@v1 id: deployment with: source-dir: ./public @@ -75,8 +75,8 @@ jobs: # checkout required for pr-preview-action to succeed, # while the content will not be used - name: Checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - - uses: rossjrw/pr-preview-action@4668d7cb417ce7067b0b59bc152b1ae1513010de # v1.4.6 + uses: actions/checkout@v4 + - uses: rossjrw/pr-preview-action@v1 id: deployment with: preview-branch: previews From fb283c641f31bb27744984467fc155d3c582a3f3 Mon Sep 17 00:00:00 2001 From: Guillaume Fontorbe Date: Fri, 1 Aug 2025 07:34:08 +0200 Subject: [PATCH 3/5] Update hugo/content/docs/concepts/data-flow.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Miro Spönemann --- hugo/content/docs/concepts/data-flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/concepts/data-flow.md b/hugo/content/docs/concepts/data-flow.md index a4c5c33..24469cb 100644 --- a/hugo/content/docs/concepts/data-flow.md +++ b/hugo/content/docs/concepts/data-flow.md @@ -11,7 +11,7 @@ Now that you understand the [Core Components]({{< relref "core-components" >}}) Here is how the unilateral data flow goes: -- An event occur. For example the application request the data model, or a user selects a node in the diagram +- An event occurs. For example, the application requests the data model, or a user selects a node in the diagram - The **Model Source** react to this event and may send the corresponding **Action** to the **Action Dispatcher** - The **Action Dispatcher** looks up the corresponding handler for the **Action** - The **Action** may be converted to a **Command** and forwarded to the **Command Stack** From c7ec11e6fed9eee813bb4d73e00d9d0285c5e3a0 Mon Sep 17 00:00:00 2001 From: Guillaume Fontorbe Date: Fri, 1 Aug 2025 07:34:17 +0200 Subject: [PATCH 4/5] Update hugo/content/docs/concepts/data-flow.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Miro Spönemann --- hugo/content/docs/concepts/data-flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugo/content/docs/concepts/data-flow.md b/hugo/content/docs/concepts/data-flow.md index 24469cb..c262c4e 100644 --- a/hugo/content/docs/concepts/data-flow.md +++ b/hugo/content/docs/concepts/data-flow.md @@ -12,7 +12,7 @@ Now that you understand the [Core Components]({{< relref "core-components" >}}) Here is how the unilateral data flow goes: - An event occurs. For example, the application requests the data model, or a user selects a node in the diagram -- The **Model Source** react to this event and may send the corresponding **Action** to the **Action Dispatcher** +- The **Model Source** reacts to this event and may send the corresponding **Action** to the **Action Dispatcher** - The **Action Dispatcher** looks up the corresponding handler for the **Action** - The **Action** may be converted to a **Command** and forwarded to the **Command Stack** - The **Command Stack** executes the **Command**, resulting in a model update and notifying the **Viewer** From bda1020307dbc8668d2ecd97930de7760d71b45d Mon Sep 17 00:00:00 2001 From: Guillaume Fontorbe Date: Thu, 28 Aug 2025 15:08:47 +0200 Subject: [PATCH 5/5] add jbicker comments --- hugo/content/docs/concepts/extension-points.md | 8 ++++++++ hugo/content/docs/recipes/custom-interactions.md | 4 ++-- hugo/content/docs/recipes/micro-layout.md | 4 ++-- hugo/content/docs/recipes/svg-rendering.md | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/hugo/content/docs/concepts/extension-points.md b/hugo/content/docs/concepts/extension-points.md index 2e24f70..98e7bb1 100644 --- a/hugo/content/docs/concepts/extension-points.md +++ b/hugo/content/docs/concepts/extension-points.md @@ -306,6 +306,8 @@ export class CreateCustomNodeCommand extends Command { **Mergeable Command:** +A *Mergeable Command* is a command that can accumulate subsequent commands of the same kind. For example, multiple subsequent move commands can be merged to yield a single command, so that they can be rolled back together by an undo. + ```typescript import { injectable, inject } from 'inversify'; import { MergeableCommand, CommandExecutionContext, CommandResult, TYPES } from 'sprotty'; @@ -381,6 +383,11 @@ export class CustomActionHandler implements IActionHandler { case 'deleteCustomNode': return new DeleteCustomNodeCommand(action.nodeId); + case 'moveCustomNode': + return new MoveCustomCommand( + action.nodeId. + action.newPosition + ) } } } @@ -403,6 +410,7 @@ const customModule = new ContainerModule((bind, unbind, isBound, rebind) => { configureCommand(context, CreateCustomNodeCommand); configureCommand(context, UpdateCustomNodeCommand); configureCommand(context, DeleteCustomNodeCommand); + configureCommand(context, MoveCustomNodeCommand); }); ``` diff --git a/hugo/content/docs/recipes/custom-interactions.md b/hugo/content/docs/recipes/custom-interactions.md index 42d29b2..71961a2 100644 --- a/hugo/content/docs/recipes/custom-interactions.md +++ b/hugo/content/docs/recipes/custom-interactions.md @@ -26,7 +26,7 @@ const container = new ContainerModule((bind, unbind, isBound, rebind) => { ``` A button handler is a simple injectable class with a `buttonPressed(button: SButton): Action[]` method. -The actions that this method returns are passed to the [`ActionDispatcher`]({{< ref "/docs/concepts/architecture-overview" >}}#action-dispatcher) to be handled there. +The actions that this method returns are passed to the [`ActionDispatcher`]({{< relref "core-components" >}}#action-dispatcher) to be handled there. ```Typescript @injectable() @@ -41,7 +41,7 @@ export class CustomButtonHandler implements IButtonHandler { ## Mouse and Keyboard Listeners Sprotty also offers the ability to attach mouse and keyboard listeners by registering `MouseListener` or `KeyListener`. -This can be simply done by binding the custom listener to the respective listener type in your [DI-container]({{< ref "/docs/concepts/extension-points">}}#dependency-injection) like this: +This can be simply done by binding the custom listener to the respective listener type in your [DI-container]({{< relref "extension-points">}}#dependency-injection) like this: ```Typescript bind(CustomMouseListener).toSelf().inSingletonScope(); diff --git a/hugo/content/docs/recipes/micro-layout.md b/hugo/content/docs/recipes/micro-layout.md index 92f839f..d1de93a 100644 --- a/hugo/content/docs/recipes/micro-layout.md +++ b/hugo/content/docs/recipes/micro-layout.md @@ -12,7 +12,7 @@ Any model element that implements or extends the `SNode` or `SCompartment` inter * `hbox`: children elements are arranged horizontally * `vbox`: children elements are arranged vertically -The `layout` property aims at arranging children elements that do not have a meaning in terms of graph hierarchy (i.e. labels, buttons, ...). Please note that children [that are instances of `SNodeImpl`]({{< ref "/docs/concepts/extension-points">}}#dependency-injection) do not respect the `layout` property by default (more on that [later](#layouting-nested-nodes)). +The `layout` property aims at arranging children elements that do not have a meaning in terms of graph hierarchy (i.e. labels, buttons, ...). Please note that children that are instances of `SNodeImpl` do not respect the `layout` property by default (more on that [later](#layouting-nested-nodes)). First and foremost, the micro-layout engine needs to be activated in the inversify container. This is done by setting the `needsClientLayout` property to `true` in the inversify container configuration: @@ -115,7 +115,7 @@ This results in the following visuals: ![layout-configuration](/assets/docs/layout-configuration.png) -If you want different layout configurations, you can implement your own micro-layout engine and inject it via [Dependency Injection]({{< ref "/docs/concepts/extension-points">}}#dependency-injection). +If you want different layout configurations, you can implement your own micro-layout engine and inject it via [Dependency Injection]({{< relref "extension-points">}}#dependency-injection). ## Layout Options diff --git a/hugo/content/docs/recipes/svg-rendering.md b/hugo/content/docs/recipes/svg-rendering.md index b1d39a7..af99b05 100644 --- a/hugo/content/docs/recipes/svg-rendering.md +++ b/hugo/content/docs/recipes/svg-rendering.md @@ -33,7 +33,7 @@ export class NodeView extends RectangularNodeView { } ``` -The class `NodeView` extends `RectangularNodeView` which is a default `View` in Sprotty, ultimately implementing `IView`. Don't forget to add the class decorator `@injectable()`, which is necessary for the [Dependency Injection]({{< ref "/docs/concepts/extension-points">}}#dependency-injection). +The class `NodeView` extends `RectangularNodeView` which is a default `View` in Sprotty, ultimately implementing `IView`. Don't forget to add the class decorator `@injectable()`, which is necessary for the [Dependency Injection]({{< relref "extension-points">}}#dependency-injection). The `render()` method is the core of the `View`. It takes `node` -- that is the model element to be rendered - as an argument, a `RenderingContext`, and an optional `args` object. View implementations should first check whether the `node` should be rendered at all. This is an optimization step, as we only want to render SVG elements that are inside of the visible viewport and not hidden by some other user-defined filter. Eventually, the `render()` method returns a `VNode` which is [Snabbdom's](https://github.com/snabbdom/snabbdom) virtual representation of a DOM element. This `VNode` can hold one and only one *root element*, therefore we need to group our SVG elements inside of a *container element* `g`. @@ -63,7 +63,7 @@ The micro-layout is computed in two phases: 1. A `RequestBoundAction` is received and the model is rendered invisibly (e.g. by assigning a width and height of zero to the elements). The locally used fonts and CSS styles are applied during this rendering phase. The resulting size information is used to invoke the selected layouts and the updated bounds are written into a `ComputedBoundAction`. 2. The bounds stored in the `ComputedBoundAction` are applied to the model and initiates the visible rendering of the updated model with `SetModelAction` or `UpdateModelAction`. -In depth documentation about the micro-layouting can be found [here]({{< ref "/docs/recipes/micro-layout" >}}) +In depth documentation about the micro-layouting can be found [here]({{< relref "micro-layout" >}}) ## Server Layout