The current version of the framework contains numerous breaking changes compared to its predecessor.
Let's, start by exploring the differences between v1 and v2, both from a developer's and user's point of view:
Context | v1 | v2 | Reference |
---|---|---|---|
TypeScript | Partial support | TypeScript-first | |
Quick start | Codepen examples | DevKit, Codesandbox examples | |
Architecture | Event-based | Middleware-like signals | |
Tools | rete-cli | rete-cli , rete-kit , rete-qa | |
Testing | unit testing | unit + E2E testing | |
UI | |||
Nodes order | fixed order | bring forward picked nodes | |
Selection | built-in for nodes only | advanced selection + custom elements | |
Controls | no built-in controls provided | built-in classic input control | |
Arrange nodes | limited | powered by elkjs | |
Code | |||
Node creation | Component-based approach | up to you | |
Editor/Engine identifiers | mandatory, required for import/export | up to you | |
Node identifier | incremental decimal id | unique id | |
Import/export | Built-in, limited | up to you | |
Validation | Socket-based validation | up to you | |
Dataflow processing | limited (no recursion) | DataflowEngine with dynamic fetching | |
Control flow processing | simulated by Task plugin with limitations | ControlFlowEngine | |
Modules | rete-module-plugin | up to you | |
Connection plugin | responsible for both rendering and interaction | responsible for interaction only |
Connect the plugin by importing it by default import.
The second parameter is used for passing the plugin's options/parameters:
// v1
import HistoryPlugin from 'rete-history-plugin';
editor.use(HistoryPlugin, { keyboard: true });
All plugins are implemented as classes and can be extended, providing flexible customization without modifying the core.
// v2
import { HistoryPlugin, HistoryExtensions, Presets } from 'rete-history-plugin'
const history = new HistoryPlugin<Schemes>()
history.addPreset(Presets.classic.setup())
HistoryExtensions.keyboard(history)
area.use(history)
In the v1, nodes are generated via components that were registered within the editor, which enabled the creation of numerous instances of nodes belonging to the same Component type.
// v1
class NumComponent extends Rete.Component {
constructor(){
super("Number");
}
builder(node) {
node.addControl(new NumControl('num'))
node.addOutput(new Rete.Output('num', "Number", numSocket))
return node
}
}
const numComponent = new NumComponent()
editor.register(numComponent);
const node = await numComponent.createNode({ num: 2 });
The current version doesn't include Component as an abstraction, but you can implement similar approach if needed.
// v2
const node = new ClassicPreset.Node('Number')
node.addControl('num', new NumControl('num'))
node.addOutput('num', new ClassicPreset.Output(numSocket, "Number"));
await editor.addNode(node)
The data can be saved using method putData
. It is expected that the data should be in a valid JSON format, as it may be used for import/export.
// v1
node.putData('myData', 'data')
control.putData('myData', 'data') // where control is part of node
There are no rigid import/export guidelines to follow in the current version, which means you have complete flexibility in how you store your data in nodes.
// v2
class MyNode extends ClassicPreset.Node {
myData = 'data'
}
Because of the limitations mentioned earlier, the editor can be effortlessly exported and imported.
// v1
const data = editor.toJSON();
await editor.fromJSON(data);
The current version incorporates a revised approach that requires implementation, as demonstrated in Import/export.
Selecting elements is a feature integrated within the editor
// v1
editor.selected.list
editor.selected.add(node, accumulate)
The downside to this implementation is its incapability to support anything other than node selection.
The selection of nodes (and other elements) looks like:
// v2
const selector = AreaExtensions.selector()
const accumulating = AreaExtensions.accumulateOnCtrl()
const nodeSelector = AreaExtensions.selectableNodes(area, selector, { accumulating });
editor.getNodes().filter(node => node.selected)
nodeSelector.select(add.id)
The typical way to listen to events that can be prevented
// v1
editor.on('nodecreate', node => {
return node.canCreate
});
* - unchanged
** - moved to different package
*** - removed
rete
package eventsrete-connection-plugin
package eventsThe current version uses a specific kind of signal implementation that involves object-based signals. Additionally, pipes are used to either manipulate these objects or prevent signal propagation.
// v2
editor.addPipe(context => {
if (context.type === 'nodecreate') return
return context
})
rete
package eventsrete-area-plugin
package eventsrete-connection-plugin
package eventsrete-angular-plugin
package eventsrete-vue-plugin
package eventsrete-react-plugin
package eventsThere is a built-in connection validation based on socket compatibility
// v1
const anyTypeSocket = new Rete.Socket('Any type');
numSocket.combineWith(anyTypeSocket);
This approach is simple but has some limitations.
Connection validation can be implemented independently, that provides more flexibility.
// v2
editor.addPipe(context => {
if (context.type === 'connectioncreate') {
if (canCreateConnection(context.data)) return false
}
return context
})
The component with defined worker
method should be registered
// v1
const engine = new Rete.Engine('[email protected]');
engine.register(myComponent);
Define worker
method of the component
// v1
worker(node, inputs, outputs){
outputs['num'] = node.data.num;
}
Trigger the processing
// v1
await engine.process(data);
Create the DataflowEngine
instance to connect to the editor. Unlike the first version, there is no need to pass data
with nodes and connections.
// v2
import { DataflowEngine } from 'rete-engine'
const engine = new DataflowEngine<Schemes>()
editor.use(engine)
Node method example
// v2
data(inputs) {
const { left, right } = inputs
return { sum: left[0] + right[0] }
}
Start the processing
// v2
engine.fetch(node.id)
This approach is implemented using the rete-task-plugin
and based on the Rete.Engine
. Therefore, it has the aforementioned limitations
// v1
import TaskPlugin from 'rete-task-plugin';
editor.use(TaskPlugin);
Component's constructor has specified outputs that are intended for control flow or dataflow
// v1
this.task = {
outputs: { exec: 'option', data: 'output' },
init(task) {
task.run('any data');
task.reset();
}
}
Define the worker
method, which returns data and specifies closed output ports for control flow
// v1
worker(node, inputs, data) {
this.closed = ['exec'];
return { data }
}
The rete-engine
package is used, which has a separate implementation of the engine for control flow
// v2
import { ControlFlowEngine } from 'rete-engine'
const engine = new ControlFlowEngine<Schemes>()
editor.use(engine)
By default, all ports are configured to pass control, but you can designate certain ones for this
// v2
const engine = new ControlFlowEngine<Schemes>(() => {
return {
inputs: () => ["exec"],
outputs: () => ["exec"]
};
});
The following serves as the node method:
// v2
execute(input: 'exec', forward: (output: 'exec') => void) {
forward('exec')
}
Unlike the previous version, this approach is completely decoupled from the dataflow. Nevertheless, it can be used in conjunction with DataflowEngine
.
// v2
async execute(input: 'exec', forward: (output: 'exec') => void) {
const inputs = await dataflow.fetchInputs(this.id)
forward('exec')
}
As a demonstration, we have opted to use rete-react-render-plugin
// v1
import ReactRenderPlugin from 'rete-react-render-plugin';
editor.use(ReactRenderPlugin)
// v2
import { ReactPlugin } from 'rete-react-plugin'
const reactPlugin = new ReactPlugin<Schemes, AreaExtra>()
area.use(reactPlugin)
The following code is used to specify the components needed for specific nodes and controls
// v1
class AddComponent extends Rete.Component {
constructor() {
super("Add");
this.data.component = MyNode;
}
}
class MyControl extends Rete.Control {
constructor(emitter, key, name) {
super(key);
this.render = 'react';
this.component = MyReactControl;
this.props = { emitter, name };
}
}
Alternatively, component can be specified for all nodes
// v1
editor.use(ReactRenderPlugin, { component: MyNode });
In this version, the components to be visualized are defined in the classic preset that is connected
// v2
reactPlugin.addPreset(ReactPresets.classic.setup({ customize: {
node(data) {
return MyNode
},
control() {
return MyReactControl
}
}}))
This approach offers greater flexibility, enabling you to define additional conditions within the handlers
Retrieve the view of the node and execute its translate
method
// v1
editor.view.nodes.get(node).translate(x, y)
The plugin instance contains translate
method that only needs the node identifier.
// v2
await area.translate(node.id, { x, y })
The plugin offers approach for positioning nodes, but its functionality is significantly restricted.
// v1
import AutoArrangePlugin from 'rete-auto-arrange-plugin';
editor.use(AutoArrangePlugin, {});
editor.trigger('arrange');
The plugin leverages the advanced functionality of the elkjs
package.
// v2
import { AutoArrangePlugin, Presets as ArrangePresets } from "rete-auto-arrange-plugin";
const arrange = new AutoArrangePlugin<Schemes>();
arrange.addPreset(ArrangePresets.classic.setup());
area.use(arrange);
await arrange.layout()
The zoomAt
method requires an editor instance that is responsible for visualization
// v1
import AreaPlugin from "rete-area-plugin";
AreaPlugin.zoomAt(editor);
For visualization purposes in this version, an instance of AreaPlugin
is required.
// v2
import { AreaExtensions } from "rete-area-plugin";
AreaExtensions.zoomAt(area, editor.getNodes());