Skip to main content

Embedding

JointJS embedding functionality allows you to organize shapes into hierarchical structures of containers (parent shapes) and children (embedded shapes). Whenever there is an interaction with a container, JointJS automatically modifies all of its embedded children too. For example, when a parent is moved, all elements embedded into that parent move as well.

Links can also be embedded into a parent. In that case, it is the link's vertices which move with the parent. Additionally, if the embedded link's source / target is connected to a point, then that point moves with the parent as well.

Automatic embedding mode​

You can enable automatic embedding on your Paper by setting the embeddingMode option in your paper definition to true.

In this mode, when you drag an element and drop into another element, the element below becomes a parent of the dropped element (the dropped element gets automatically embedded into the parent element). Similarly, when you drag an element out of its parent, the element gets automatically unembedded from its parent.

To control what shapes can be embedded into what other shapes, you can use the validateEmbedding() callback option on the paper. This is useful for hierarchical diagrams. By default, all elements can be embedded as children to all other elements.

Conversely, you can also control what shapes are allowed to be unembedded from their parents with the validateUnembedding() callback option on the paper. By default, all embedded elements can be unembedded.

To control the other side of the equation - i.e. which element is offered as a potential parent for an element that is being dragged over the paper - you can use the findParentBy() callback option on the paper. By default, the element with the highest z-index (the topmost one) under the dragged element view's bounding box is chosen. (Note that if frontParentOnly is false, then all elements under the dragged element may be considered according to the provided logic - not only the one with the highest z-index - as outlined below.)

In addition, you may override the frontParentOnly option on the paper. By default, only the element with the highest z-index under the dragged element is considered (according to the rules of the findParentBy() function as outlined above). However, when frontParentOnly is set to false, the elements under the dragged element view are tested one by one in order of decreasing z-index (from front to back) until a valid parent is found. This is relevant if you want to be able to drop children into a parent which overlap other children already embedded in the parent element.

The following example illustrates the Paper embedding mode. Circular elements can be embedded into rectangular elements by dragging-and-dropping, and one rectangular element can be embedded into the other as well (this is enforced via the validateEmbedding() callback option). Double-clicking a container resizes it to fit tightly around its children (if any):

Adding an embedded child​

The cell.embed() method allows you to embed an element or link (or an array of these) into a given shape (cell). The shape then becomes the parent of the embedded cells:

parent.embed([childElement, childLink]);
info

You can only embed shapes which are not currently embedded; trying to do so causes an exception to be thrown: Embedding of already embedded cells is not allowed..

If you need to override this check, unembed the shapes first.

Removing an embedded child​

The cell.unembed() method allows you to remove a given element or link (or an array of these) from a given shape:

parent.unembed([childElement, childLink]);

Events​

Whenever the embeds change on an element, JointJS triggers the 'change:embeds' event on it (and on the containing Graph). The embeds argument of the callback contains an array of the embedded shapes after the change:

graph.on('change:embeds', (parent, embeds, opt) => {
const childIds = embeds.map((child) => child.id);
parent.attr('label/text': `Children:\n${childIds.join('\n')}`);
});

Similarly, whenever the parent of an element changes, JointJS triggers the 'change:parent' event on it (and on the containing Graph). Note that parent can be null if the child was unembedded:

graph.on('change:parent', (child, parent, opt) => {
child.attr('label/text': `Parent: ${parent?.id}`);
});

Working with cell hierarchy​

JointJS provides several methods that allow you to take the cell hierarchy into account in your logic:

  • cell.isEmbedded() returns true when this cell is embedded.
  • cell.isEmbeddedIn(parent, opt) returns true when this cell is embedded in the provided parent. By default, this method checks whether parent is an ancestor of cell. If you need to check direct parentage only, you can pass opt.deep with a value of false.
  • cell.getParentCell() returns the parent of this cell (or null if it is not embedded). (If you only need the parent's id, you can use the cell.parent() instead.)
    • cell.getAncestors() returns an array of all ancestors of this cell starting from the immediate parent all the way up to the most distant ancestor.
  • cell.getEmbeddedCells(opt) returns an array of the embedded children of this cell. (If you only need an array of the children's id values, you can use cell.get(embeds) instead.)
    • If opt.deep is true, all deeply embedded cells will be returned. The order in which the cells are returned depends on the search algorithm used. By default, Depth-first search (DFS) algorithm is used. If opt.breadthFirst is true, the Breadth-first search (BFS) algorithm will be used instead.

Some embedding methods only make sense in the context of Elements or Links, respectively. Those are:

  • element.fitParent(opt) resizes and repositions this element's parent so that it contains all of its children (including this element) within its bounding box.
    • If opt.deep is true, all ancestors of this element are resized and repositioned based on their children, starting with the parent and proceeding up through the cell hierarchy.
  • element.fitToChildren(opt) resizes and repositions this element so that it contains all of its children within its bounding box.
    • If opt.deep is true, all descendants of this element are resized and repositioned based on their children, starting from the bottommost descendants and proceeding up through the cell hierarchy to this element.
  • link.reparent() changes the parent of the link to the common ancestor (see below) of the link's source and target elements. This is usually the most intuitive way to handle embedded links.

Finally, one Graph method allows you to work with embeddings at a higher level:

note

In addition, many Graph methods (e.g. graph.getSubgraph(), graph.getConnectedLinks()) also accept the deep option to specify that all embedding descendants of the provided cells need be taken into account as well.

info

You may be confused by the concepts of ancestors / descendants vs. predecessors / successors. The difference is the following:

  • Ancestors (parent) and descendants (children) describe cell hierarchy as defined by embeddings. This is the subject of this article.
  • Predecessors and successors (in general, neighbors) describe graph structure as defined by links between elements.

Collapse & Expand​

The following demo shows how the embedding functionality can be used together with Paper's viewport option to create collapsible container shapes: