Import/export - Rete.js

Import/export

ModulesData structures3D Configurator

Out of the box, the editor can handle any JS objects as nodes and connections, as long as they include a mandatory id field for both. Additionally, connections must have source and target fields. These objects can be either plain data objects or objects that include methods. Check out the Data structures guide for details.

The current version of the editor doesn't support importing/exporting by default due to some limitations:

  • The process of serializing nodes/connections to JSON can be challenging
  • The import order of nodes may vary depending on how your graph is structured

Valid JSON objects

Suppose we have a straightforward example where the node is a valid JSON object

ts
import { NodeEditor, BaseSchemes, getUID } from 'rete'

const editor = new NodeEditor<BaseSchemes>()

const node = { id: getUID(), label: 'Label' }

await editor.addNode(node)

In this case, we can easily export such nodes into JSON to save them in a database, for instance.

Non-valid JSON objects

Objects that aren't valid JSON, such as instances of classes, objects with functions, or objects with cyclic references pose a challenge. Discarding them would mean losing the advantages that come with using JS.

For instance, node classes can be used to create nodes, allowing for more reliable and convenient interaction through methods, and providing the flexibility to use different paradigms.

ts
import { ClassicPreset } from 'rete'

const node = new ClassicPreset.Node('Label')

node.addOutput('port', new ClassicPreset.Output(socket, 'Label'))

await editor.addNode(node)

While serialization and deserialization might be one way to convert such objects into valid JSON, this approach may not work in complex scenarios.

Exporting and importing nodes

If you want to export a graph, you can use the following code as a reference. Please note that this is not a fully functional code, but rather an approximation to help you implement your own import/export functionality according to your specific requirements.

ts
const data = { nodes: [] }
const nodes = editor.getNodes()

for (const node of nodes) {
  data.nodes.push({
    id: node.id,
    label: node.label,
    inputs: /// ....
    controls: /// ....
    outputs: /// ....
  })
}

In order to revert the transformation, we must initialize node instances, inputs, outputs, and controls based on the objects provided.

ts
for (const { id, label, inputs, outputs, controls } of data.nodes) {
  const node = new ClassicPreset.Node(label);

  node.id = id;

  /// ... inputs
  /// ... controls
  /// ... outputs

  await editor.addNode(node)
}

The complete example can be found at the link. Note that this example has been simplified for ease of understanding.

Additionally, importing or exporting inputs and outputs may not always be necessary if they are static and we know the node type. In such cases, we can simply save the node name and relevant data, which can be used to create instances of nodes with pre-defined ports. Check out the 3D Configurator and the Modules examples with implementation of this approach.

Import nodes order

Another challenge during graph import could be the order in which nodes need to be imported. In a simple cases, the order will be the same as the order in which they were added to the editor.

ts
const graph = /// loaded JSON-valid object from DB

for (const node of graph.nodes) {
  await editor.addNode(node)
}

When dealing with more complex graphs, the order of adding nodes can vary. For instance, in a graph with nested nodes, it might be necessary to add parent nodes prior to child nodes. Moreover, it's quite possible that a user may create a child node before its parent node while working in the editor.

Let's take a look at an example of importing a graph where certain nodes have parent field indicating their association with another node. As a result, these nodes must be imported after their parent node has been created.

ts
async function importForParent(nodes, parent = undefined) {
  const nodes = nodes.filter(node => node.parent === parent)

  for (const node of nodes) {
    await editor.addNode(node)
    await importForParent(nodes, node.id)
  }
}

const graph = /// loaded JSON-valid object from DB

await importForParent(graph.nodes)

Since this approach is more complex and there are multiple ways to do it, the import method will vary depending on your specific use case.