Dependency Injection
It’s time to wire everything together! In this section, you’ll learn how to use dependency injection to connect your model and views, creating a cohesive diagram application.
Dependency injection is a design pattern that helps manage complex applications by:
- Decoupling components - Each part of your application can be developed and tested independently
- Centralizing configuration - All connections between components are defined in one place
- Enabling extensibility - New components can be easily added without changing existing code
π‘ Key Insight: Sprotty uses InversifyJS, a powerful dependency injection framework for TypeScript, to manage its components.
Let’s create a configuration that connects your model elements to their view implementations and sets up the diagram viewer.
Create a new file called di.config.ts
in your src
directory:
import { Container, ContainerModule } from 'inversify';
import {
configureModelElement, configureViewerOptions, ConsoleLogger, loadDefaultModules,
LocalModelSource, LogLevel, PolylineEdgeView, RectangularNode, SEdgeImpl,
SGraphImpl, SGraphView, SRoutingHandleImpl, SRoutingHandleView, TYPES
} from 'sprotty';
import { TaskNodeView } from './views';
export default (containerId: string) => {
const myModule = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope();
rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope();
rebind(TYPES.LogLevel).toConstantValue(LogLevel.log);
const context = { bind, unbind, isBound, rebind };
configureModelElement(context, 'graph', SGraphImpl, SGraphView);
configureModelElement(context, 'task', RectangularNode, TaskNodeView);
configureModelElement(context, 'edge', SEdgeImpl, PolylineEdgeView);
configureModelElement(context, 'routing-point', SRoutingHandleImpl, SRoutingHandleView);
configureModelElement(context, 'volatile-routing-point', SRoutingHandleImpl, SRoutingHandleView);
configureViewerOptions(context, {
needsClientLayout: false,
baseDiv: containerId
});
});
const container = new Container();
loadDefaultModules(container);
container.load(myModule);
return container;
}
Let’s break down this configuration into manageable parts:
const myModule = new ContainerModule((bind, unbind, isBound, rebind) => {
// Configuration goes here
});
This creates a new InversifyJS module that will contain all our bindings. The function parameters provide methods to register and manage dependencies.
bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope();
rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope();
rebind(TYPES.LogLevel).toConstantValue(LogLevel.log);
These lines configure essential Sprotty services:
ModelSource
- Provides the model data (we’re usingLocalModelSource
for client-only diagrams)ILogger
- Handles logging (we’re usingConsoleLogger
to log to the browser console)LogLevel
- Sets the logging verbosity
const context = { bind, unbind, isBound, rebind };
configureModelElement(context, 'graph', SGraphImpl, SGraphView);
configureModelElement(context, 'task', RectangularNode, TaskNodeView);
configureModelElement(context, 'edge', SEdgeImpl, PolylineEdgeView);
configureModelElement(context, 'routing-point', SRoutingHandleImpl, SRoutingHandleView);
configureModelElement(context, 'volatile-routing-point', SRoutingHandleImpl, SRoutingHandleView);
This is where the magic happens! Each configureModelElement
call maps a model element type to:
- A model implementation class that handles the element’s behavior and data
- A view implementation class that handles the element’s rendering
Type | Model Implementation | View Implementation | Purpose |
---|---|---|---|
‘graph’ | SGraphImpl | SGraphView | The root diagram element |
’task' | RectangularNode | TaskNodeView | Our custom task nodes |
’edge' | SEdgeImpl | PolylineEdgeView | Connections between tasks |
‘routing-point’ | SRoutingHandleImpl | SRoutingHandleView | Handles for edge routing |
‘volatile-routing-point’ | SRoutingHandleImpl | SRoutingHandleView | Handles for edge routing |
configureViewerOptions(context, {
needsClientLayout: false,
baseDiv: containerId
});
This configures options for the diagram viewer:
needsClientLayout: false
- We’re providing explicit positions for our elementsbaseDiv: containerId
- The ID of the HTML element where the diagram will be rendered
const container = new Container();
loadDefaultModules(container);
container.load(myModule);
return container;
Finally, we:
- Create a new InversifyJS container
- Load Sprotty’s default modules (providing core functionality)
- Load our custom module
- Return the configured container
The most important concept to understand is how Sprotty uses the type string to map model elements to their implementations and views:
- In your model, you specify a
type
property (e.g.,type: 'task'
) - In your DI configuration, you map that type to implementations (e.g.,
configureModelElement(context, 'task', RectangularNode, TaskNodeView)
) - When Sprotty renders your diagram, it uses this mapping to find the right view for each element
This powerful mechanism allows you to:
- Use different views for the same model element type in different contexts
- Extend existing element types with new behaviors
- Create completely custom element types with specialized rendering
After adding dependency injection, your project structure has grown to include:
hello-world/
βββ node_modules/ # Dependencies installed by npm
βββ package.json # Project configuration
βββ package-lock.json # Dependency lock file
βββ tsconfig.json # TypeScript configuration
βββ src/
β βββ model.ts # Custom node type definitions
β βββ model-source.ts # Diagram model data structure
β βββ views.tsx # Custom view implementations
β βββ di.config.ts # Dependency injection configuration
βββ static/
βββ index.html # HTML entry point
βββ styles.css # CSS styling for the diagram
The new file we’ve created in this section:
src/di.config.ts
- The dependency injection configuration that connects our models to views and sets up the diagram infrastructure
This configuration is the “glue” that ties all of our components together, creating a coherent system where:
- Models provide the data structure
- Views handle the rendering
- Dependency injection connects them according to type mappings
In the next section, we’ll put everything together to create a working diagram application.