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]);
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()
returnstrue
when thiscell
is embedded.cell.isEmbeddedIn(parent, opt)
returnstrue
when thiscell
is embedded in the providedparent
. By default, this method checks whetherparent
is an ancestor ofcell
. If you need to check direct parentage only, you can passopt.deep
with a value offalse
.cell.getParentCell()
returns the parent of thiscell
(ornull
if it is not embedded). (If you only need the parent'sid
, you can use thecell.parent()
instead.)cell.getAncestors()
returns an array of all ancestors of thiscell
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 thiscell
. (If you only need an array of the children'sid
values, you can usecell.get(embeds)
instead.)- If
opt.deep
istrue
, 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. Ifopt.breadthFirst
istrue
, the Breadth-first search (BFS) algorithm will be used instead.
- If
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
istrue
, all ancestors of this element are resized and repositioned based on their children, starting with the parent and proceeding up through the cell hierarchy.
- If
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.
- If
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:
graph.getCommonAncestor(...cells)
returns the closest ancestor shared by the provided cells.
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.
You may be confused by the concepts of ancestors
/ descendants
vs. predecessors
/ successors
. The difference is the following:
Ancestors
(parent
) anddescendants
(children
) describe cell hierarchy as defined by embeddings. This is the subject of this article.Predecessors
andsuccessors
(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: