<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ramy Othman</title>
    <description>The latest articles on DEV Community by Ramy Othman (@ramy_othman).</description>
    <link>https://dev.to/ramy_othman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101566%2F943518a4-ccb8-4ccb-bad0-f86d168e24f8.png</url>
      <title>DEV Community: Ramy Othman</title>
      <link>https://dev.to/ramy_othman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ramy_othman"/>
    <language>en</language>
    <item>
      <title>Auto-layout you don't have to build</title>
      <dc:creator>Ramy Othman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:12:34 +0000</pubDate>
      <link>https://dev.to/ramy_othman/auto-layout-you-dont-have-to-build-1311</link>
      <guid>https://dev.to/ramy_othman/auto-layout-you-dont-have-to-build-1311</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on the &lt;a href="https://grafloria.com/blog/auto-layout-you-dont-have-to-build/" rel="noopener noreferrer"&gt;Grafloria engineering blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a special kind of feature request: the one filed as &lt;a href="https://github.com/xyflow/xyflow/issues/5" rel="noopener noreferrer"&gt;issue #5&lt;/a&gt; of a project and still generating tutorials years later. For flow libraries that's auto-layout. The mainstream answer became "integrate dagre or elk yourself", and an entire genre of blog posts exists to walk you through the wiring — graph conversion in, position mapping out, edge cases yours forever. (The sequel, &lt;a href="https://github.com/xyflow/xyflow/issues/1113" rel="noopener noreferrer"&gt;"is dynamic auto layouting possible?"&lt;/a&gt;, has 28 comments of people discovering the hard parts.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the wiring tutorials don't tell you
&lt;/h2&gt;

&lt;p&gt;Getting dagre to emit positions is the easy afternoon. The parts that eat the week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Which algorithm, when?&lt;/strong&gt; Layered algorithms are right for DAGs and wrong&lt;br&gt;
for hairballs; trees want tree layouts; a picker is part of the feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ELK is 1.4&amp;nbsp;MB.&lt;/strong&gt; The best layered engine is also the heaviest — ship it&lt;br&gt;
eagerly and your bundle audit will find it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The second layout.&lt;/strong&gt; Insert a node into a laid-out graph and a full re-run&lt;br&gt;
scrambles every position the user has memorized. You need &lt;em&gt;incremental&lt;/em&gt; layout, and no wiring tutorial covers it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't fight the hands.&lt;/strong&gt; If layout re-runs on every data change, it undoes&lt;br&gt;
the drag your user just made. When it runs is as important as what it computes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One call, and the four problems it absorbs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;elk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                                    &lt;span class="c1"&gt;// picker: 'auto' inspects the graph&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dagre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rankSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;layoutIncremental&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inserted-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// the second layout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registered names: &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;elk&lt;/code&gt;, &lt;code&gt;dagre&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;layered&lt;/code&gt;, &lt;code&gt;tree&lt;/code&gt;, &lt;code&gt;grid&lt;/code&gt;, &lt;code&gt;circular&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;radial&lt;/code&gt;, &lt;code&gt;force&lt;/code&gt;, &lt;code&gt;spectral&lt;/code&gt;, &lt;code&gt;community&lt;/code&gt;.&lt;br&gt;
ELK loads as a lazy chunk on first use — it costs your bundle nothing until someone actually invokes it — and runs off-thread in a Worker. And in every framework binding, the declarative form re-runs on &lt;em&gt;prop-value&lt;/em&gt; change only, never on node data, so it structurally cannot fight a user's drag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React&lt;/span&gt;

&lt;span class="c1"&gt;// Angular&lt;/span&gt;

&lt;span class="c1"&gt;// Vue&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See every algorithm on a real graph: &lt;a href="https://grafloria.com/demos/#layout" rel="noopener noreferrer"&gt;layout demos&lt;/a&gt; · the concept guide (including the choosing table): &lt;a href="https://grafloria.com/learn/layout/" rel="noopener noreferrer"&gt;Layout&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — &lt;a href="https://grafloria.com/" rel="noopener noreferrer"&gt;grafloria.com&lt;/a&gt;. If this post was useful, the &lt;a href="https://grafloria.com/demos/" rel="noopener noreferrer"&gt;demo gallery&lt;/a&gt; is where the ideas live as running code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>angular</category>
      <category>vue</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Build a workflow editor in Vue 3 — the whole thing</title>
      <dc:creator>Ramy Othman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:12:28 +0000</pubDate>
      <link>https://dev.to/ramy_othman/build-a-workflow-editor-in-vue-3-the-whole-thing-4602</link>
      <guid>https://dev.to/ramy_othman/build-a-workflow-editor-in-vue-3-the-whole-thing-4602</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on the &lt;a href="https://grafloria.com/blog/build-a-workflow-editor-in-vue/" rel="noopener noreferrer"&gt;Grafloria engineering blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vue gets fewer diagram tutorials than React, and the ones that exist usually stop at "nodes appear on screen". This one goes where real workflow editors go: custom node cards, ports with connection rules, ⌘Z, auto-layout, and a save format — in Vue idiom the whole way. Everything below is running code; it's the same API our Vue demo gallery exercises in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  1 — A canvas with taste
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
import { GrafloriaFlow } from '@grafloria/vue';

const nodes = [
  { id: 'trigger', type: 'step', position: { x: 60, y: 120 },
    size: { width: 220, height: 90 }, data: { title: 'New signup', kind: 'Trigger' } },
  { id: 'email', type: 'step', position: { x: 380, y: 120 },
    size: { width: 220, height: 90 }, data: { title: 'Send welcome email', kind: 'Action' } },
];
const edges = [{ id: 'e1', source: 'trigger', target: 'email' }];

          {{ data.kind }}
          {{ data.title }}

.step { height: 100%; background: #fff; border: 1.5px solid #94A5F0; border-radius: 12px;
        padding: 10px 14px; box-sizing: border-box; }
.kind { font-size: 11px; font-weight: 600; color: #3B52D9; text-transform: uppercase; }
.title { font-weight: 700; }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;#node-step&lt;/code&gt; slot renders every node of type &lt;code&gt;step&lt;/code&gt; — declaring the slot is the whole custom-node opt-in. &lt;code&gt;:plugins="true"&lt;/code&gt; mounts the minimap, zoom controls and dotted background (lazy-loaded). Give the wrapper a real height: the canvas fills its container, and 100% of zero is a blank page.&lt;/p&gt;

&lt;h2&gt;
  
  
  2 — Ports and the rules of connection
&lt;/h2&gt;

&lt;p&gt;A workflow editor without connection rules teaches users to build broken flows. Ports carry direction; validators carry your domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerConnectionValidator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clearConnectionValidators&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grafloria/element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onBeforeUnmount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trigger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Trigger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;t-out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Send welcome email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e-in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e-out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onInit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dispose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerConnectionValidator&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;sourceNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetNode&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kind&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Trigger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a Trigger has no inputs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEngine&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setInteractionConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;portVisibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;always&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;onBeforeUnmount&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;?.();&lt;/span&gt; &lt;span class="nf"&gt;clearConnectionValidators&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it with &lt;code&gt;@init="onInit"&lt;/code&gt;. An &lt;code&gt;input&lt;/code&gt; port refuses to start a wire, the validator vetoes with a reason your user sees, and — the part you never have to build — &lt;strong&gt;⌘Z already works&lt;/strong&gt;: every gesture is one command on the engine's history.&lt;/p&gt;

&lt;h2&gt;
  
  
  3 — Layout and persistence
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stop hand-placing steps: one prop&lt;/span&gt;
&lt;span class="c1"&gt;//    (or "elk" for gnarlier graphs)&lt;/span&gt;

&lt;span class="c1"&gt;// save — the whole diagram is one document:&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DiagramSerializer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grafloria/element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DiagramSerializer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getModel&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// text for humans: the same flow as valid Mermaid, positions preserved&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exportText&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://grafloria.com/learn/vue/" rel="noopener noreferrer"&gt;10-minute Vue tutorial&lt;/a&gt; covers the same ground interactively; the &lt;a href="https://grafloria.com/learn/vue-custom-nodes/" rel="noopener noreferrer"&gt;Vue deep guides&lt;/a&gt; go into slots, state and dashboards; and every demo in the gallery exists as &lt;a href="https://grafloria.com/demos-vue/" rel="noopener noreferrer"&gt;a real Vue SFC with source shown&lt;/a&gt;. If you're coming from Vue Flow, the &lt;a href="https://grafloria.com/compare/#diagrams" rel="noopener noreferrer"&gt;sourced comparison&lt;/a&gt; shows exactly where the boxes differ.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — &lt;a href="https://grafloria.com/" rel="noopener noreferrer"&gt;grafloria.com&lt;/a&gt;. If this post was useful, the &lt;a href="https://grafloria.com/demos/" rel="noopener noreferrer"&gt;demo gallery&lt;/a&gt; is where the ideas live as running code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Custom nodes without the props mystery</title>
      <dc:creator>Ramy Othman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:12:14 +0000</pubDate>
      <link>https://dev.to/ramy_othman/custom-nodes-without-the-props-mystery-3g6k</link>
      <guid>https://dev.to/ramy_othman/custom-nodes-without-the-props-mystery-3g6k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on the &lt;a href="https://grafloria.com/blog/custom-nodes-without-the-props-mystery/" rel="noopener noreferrer"&gt;Grafloria engineering blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two of the most-viewed custom-node questions on Stack Overflow — &lt;a href="https://stackoverflow.com/questions/71586417/" rel="noopener noreferrer"&gt;"Can you pass props to a custom node?"&lt;/a&gt; (9.7k views) and &lt;a href="https://stackoverflow.com/questions/73508204/" rel="noopener noreferrer"&gt;"saving additional data to a node after it has been created"&lt;/a&gt; (25k views) — are really one question asked from two directions: &lt;strong&gt;who owns a node's data?&lt;/strong&gt; Get that model wrong and every read and write becomes a mystery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;A node has exactly one data home: the &lt;code&gt;data&lt;/code&gt; dictionary on the node itself, in the diagram's model. Your component doesn't receive "props you passed" — it receives &lt;em&gt;the node's data&lt;/em&gt;, and updating means writing to the node, not to a component. Once that clicks, every framework binding is the same pattern wearing local idiom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The spec — identical in all three frameworks:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Build&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;     &lt;span class="c1"&gt;// ← the one data home&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;React&lt;/strong&gt; — a component per type via &lt;code&gt;nodeTypes&lt;/code&gt;, receiving&lt;br&gt;
&lt;code&gt;{ id, data, selected, node }&lt;/code&gt; (mark the spec &lt;code&gt;custom: true&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Vue&lt;/strong&gt; — a named slot per type; declaring the slot &lt;em&gt;is&lt;/em&gt; the opt-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
    {{ data.title }}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Angular&lt;/strong&gt; — an ng-template per type, same auto-opt-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
    {{ data['title'] }}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And the 25k-view question: writing data later
&lt;/h2&gt;

&lt;p&gt;Because the node owns its data, "saving additional data after creation" is a write to the node — tracked for undo and events like any other model change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getModel&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;passing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// undoable, observable, serialized&lt;/span&gt;
&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderNow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No syncing a parallel store, no cloning the nodes array to smuggle a field in. The escape hatch is symmetric: your component/template also receives the live&lt;br&gt;
&lt;code&gt;node&lt;/code&gt;, so reads that outgrow &lt;code&gt;data&lt;/code&gt; have somewhere to go.&lt;/p&gt;

&lt;p&gt;Deep guides per framework: &lt;a href="https://grafloria.com/learn/react-custom-nodes/" rel="noopener noreferrer"&gt;React&lt;/a&gt; · &lt;a href="https://grafloria.com/learn/vue-custom-nodes/" rel="noopener noreferrer"&gt;Vue&lt;/a&gt; · &lt;a href="https://grafloria.com/learn/angular-custom-nodes/" rel="noopener noreferrer"&gt;Angular&lt;/a&gt; · &lt;a href="https://grafloria.com/learn/javascript-custom-nodes/" rel="noopener noreferrer"&gt;plain JavaScript&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — &lt;a href="https://grafloria.com/" rel="noopener noreferrer"&gt;grafloria.com&lt;/a&gt;. If this post was useful, the &lt;a href="https://grafloria.com/demos/" rel="noopener noreferrer"&gt;demo gallery&lt;/a&gt; is where the ideas live as running code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>angular</category>
      <category>vue</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Draggable edges that stay routed</title>
      <dc:creator>Ramy Othman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:11:58 +0000</pubDate>
      <link>https://dev.to/ramy_othman/draggable-edges-that-stay-routed-1bo</link>
      <guid>https://dev.to/ramy_othman/draggable-edges-that-stay-routed-1bo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on the &lt;a href="https://grafloria.com/blog/draggable-edges-that-stay-routed/" rel="noopener noreferrer"&gt;Grafloria engineering blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The single most-voted react-flow question on Stack Overflow is disarmingly simple: &lt;a href="https://stackoverflow.com/questions/77831116/is-it-possible-to-shape-reactflow-edges-by-dragging-them" rel="noopener noreferrer"&gt;"Is it possible to shape reactflow edges by dragging them?"&lt;/a&gt; The accepted reality in the answers: not built in — write a custom edge, manage your own control points. Anyone who has built a wiring UI knows why users keep asking: the router's idea of a good path and a human's idea of a &lt;em&gt;readable&lt;/em&gt; path are different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just add drag handles" fails
&lt;/h2&gt;

&lt;p&gt;The naive version stores pixel positions for a bend and draws through them. It works until anything moves. Then you face the real question: when the router recomputes — because a node moved, because an obstacle appeared — what happens to the user's bend?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If the router &lt;strong&gt;discards&lt;/strong&gt; it, users learn their edits don't stick and stop&lt;br&gt;
making them. (This is the classic smart-router-vs-segment-tool conflict — the same one JointJS users hit when the Manhattan router and the vertex tool fight over the same path.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the router &lt;strong&gt;defers&lt;/strong&gt; to it entirely, the edge happily runs through the&lt;br&gt;
node your bend now overlaps, and the diagram starts lying.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bends as constraints
&lt;/h2&gt;

&lt;p&gt;The version that works treats a user's bend as a &lt;strong&gt;waypoint in the model&lt;/strong&gt; — serialized with the edge, undoable like any edit, and handed to the router as a constraint: route &lt;em&gt;through&lt;/em&gt; these points, avoid obstacles &lt;em&gt;between&lt;/em&gt; them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;r1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;c1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orthogonal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;avoid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// A user dragged the wire here once. It survives node drags, re-routes,&lt;/span&gt;
  &lt;span class="c1"&gt;// serialization, and undo — the router works around it, not over it.&lt;/span&gt;
  &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;340&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Grafloria you don't write the drag handling: grab any segment of a routed edge and bend it; the bend lands in &lt;code&gt;points&lt;/code&gt;; the &lt;code&gt;avoid&lt;/code&gt; router keeps dodging nodes on the spans between your waypoints. Delete the bend and the router owns the whole path again. The mechanics of that router — the obstacle index, the grid search, what a turn costs — are in &lt;a href="https://grafloria.com/blog/obstacle-avoiding-edge-routing/" rel="noopener noreferrer"&gt;their own deep-dive&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details users actually feel
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fat hit targets.&lt;/strong&gt; A 2px line is a 2px click target — another&lt;br&gt;
&lt;a href="https://github.com/xyflow/xyflow/issues/1211" rel="noopener noreferrer"&gt;long-lived request&lt;/a&gt; elsewhere. Grafloria edges hit-test against a widened invisible band, so grabbing a wire doesn't require surgical aim.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Labels ride along.&lt;/strong&gt; An edge label is anchored to the path and draggable&lt;br&gt;
itself; its position persists in the model too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One undo per gesture.&lt;/strong&gt; A bend, a reconnect, a label move — each is one&lt;br&gt;
command, so ⌘Z behaves the way muscle memory expects.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it: &lt;a href="https://grafloria.com/demos/edges/editable-edge.html" rel="noopener noreferrer"&gt;the editable-edge demo&lt;/a&gt; · the concepts: &lt;a href="https://grafloria.com/learn/edges-and-routing/" rel="noopener noreferrer"&gt;Edges &amp;amp; routing&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — &lt;a href="https://grafloria.com/" rel="noopener noreferrer"&gt;grafloria.com&lt;/a&gt;. If this post was useful, the &lt;a href="https://grafloria.com/demos/" rel="noopener noreferrer"&gt;demo gallery&lt;/a&gt; is where the ideas live as running code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>The Angular diagram-library problem, and how not to get stranded again</title>
      <dc:creator>Ramy Othman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:11:49 +0000</pubDate>
      <link>https://dev.to/ramy_othman/the-angular-diagram-library-problem-and-how-not-to-get-stranded-again-1766</link>
      <guid>https://dev.to/ramy_othman/the-angular-diagram-library-problem-and-how-not-to-get-stranded-again-1766</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on the &lt;a href="https://grafloria.com/blog/the-angular-diagram-library-graveyard/" rel="noopener noreferrer"&gt;Grafloria engineering blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you maintain an Angular app that draws graphs, there's a decent chance you've lived this story. The most popular Angular-native graph library's top-reacted issue is &lt;a href="https://github.com/swimlane/ngx-graph/issues/481" rel="noopener noreferrer"&gt;"Angular 15 not supported"&lt;/a&gt; — open for years, twenty-three thumbs up, followed by the same request for 17, 14, and 12 before it. A &lt;a href="https://github.com/swimlane/ngx-graph/issues/78" rel="noopener noreferrer"&gt;rendering bug from 2018&lt;/a&gt; is still open. Meanwhile the Stack Overflow history under its tag is install failures on each new Angular major.&lt;/p&gt;

&lt;p&gt;To be clear about tone: this is not a dunk. Open-source maintenance is unpaid work, the maintainers gave the community something real, and every library — ours included — is one life-change away from the same curve. The useful question isn't "whose fault", it's: &lt;strong&gt;what should an Angular team demand from a diagram dependency so this can't strand them again?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap has a shape
&lt;/h2&gt;

&lt;p&gt;Angular's major-version cadence means a UI library that misses two majors becomes uninstallable in new projects (peer-dependency resolution simply refuses), and the workarounds — &lt;code&gt;--legacy-peer-deps&lt;/code&gt;, forks, vendoring — each transfer the maintenance burden to you. The other escape hatch we keep seeing is &lt;a href="https://www.reddit.com/r/angular/comments/1innh2x/" rel="noopener noreferrer"&gt;wrapping a React library in Angular&lt;/a&gt;, which means shipping two frameworks and bridging every event across the boundary. It works. You will not enjoy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four demands to make of any diagram library
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A peer range that spans majors, on record.&lt;/strong&gt; Not a README promise — the&lt;br&gt;
&lt;code&gt;package.json&lt;/code&gt;. (Ours currently declares&lt;br&gt;
&lt;code&gt;^18.1.0 || ^19.0.0 || ^20.0.0&lt;/code&gt;.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Proof the examples run, on every release.&lt;/strong&gt; A gallery of screenshots proves&lt;br&gt;
nothing about the current version. Every Grafloria demo is executed in CI with real pointer events on every commit — an example that breaks blocks the release. Ask any vendor: &lt;em&gt;what fails your build if a demo breaks?&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A framework-agnostic core.&lt;/strong&gt; If the engine has no framework dependency, the&lt;br&gt;
Angular binding is a thin layer that's cheap to keep current — and if the worst happens, your diagram logic survives a framework migration. Our Angular layer is components and signals over the same engine the React, Vue and vanilla builds use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Full source under a real license.&lt;/strong&gt; MIT means the ultimate fallback — you&lt;br&gt;
can maintain it — is at least possible. It's the difference between a risk and a cliff.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Angular-native actually looks like now
&lt;/h2&gt;

&lt;p&gt;Signal-based two-way bindings, templates for custom nodes, zoneless-verified — no wrappers around another framework's runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DiagramCanvasComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grafloria/angular&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DiagramCanvasComponent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`

  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FlowComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;edges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drag, connect, pan/zoom, minimap, ⌘Z — wired. Every gallery demo exists as a real Angular component with source shown: &lt;a href="https://grafloria.com/demos-angular/" rel="noopener noreferrer"&gt;the Angular demos&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ten-minute start: &lt;a href="https://grafloria.com/learn/angular/" rel="noopener noreferrer"&gt;the Angular tutorial&lt;/a&gt; · the honest shortlist including ngx-vflow and the commercial suites: &lt;a href="https://grafloria.com/alternatives/" rel="noopener noreferrer"&gt;alternatives&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — &lt;a href="https://grafloria.com/" rel="noopener noreferrer"&gt;grafloria.com&lt;/a&gt;. If this post was useful, the &lt;a href="https://grafloria.com/demos/" rel="noopener noreferrer"&gt;demo gallery&lt;/a&gt; is where the ideas live as running code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Sub-flows, groups, containers: why nesting breaks every flow library</title>
      <dc:creator>Ramy Othman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:11:35 +0000</pubDate>
      <link>https://dev.to/ramy_othman/sub-flows-groups-containers-why-nesting-breaks-every-flow-library-5hk6</link>
      <guid>https://dev.to/ramy_othman/sub-flows-groups-containers-why-nesting-breaks-every-flow-library-5hk6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on the &lt;a href="https://grafloria.com/blog/why-nesting-breaks-flow-libraries/" rel="noopener noreferrer"&gt;Grafloria engineering blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Look at the top-voted feature requests of any flow library and one theme towers over the rest: users want to put things &lt;em&gt;inside&lt;/em&gt; other things. In React Flow's tracker it's &lt;a href="https://github.com/xyflow/xyflow/issues/1377" rel="noopener noreferrer"&gt;"Nested flows"&lt;/a&gt;, &lt;a href="https://github.com/xyflow/xyflow/issues/611" rel="noopener noreferrer"&gt;"Nested Nodes"&lt;/a&gt; and &lt;a href="https://github.com/xyflow/xyflow/issues/3393" rel="noopener noreferrer"&gt;"absolute positioning in sub flows"&lt;/a&gt; — sixty-plus upvotes between them, one still open. This isn't a React Flow problem. It's that nesting looks like a styling feature and is actually a&lt;br&gt;
&lt;em&gt;semantics&lt;/em&gt; feature, and half-shipping it is worse than not shipping it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it's genuinely hard
&lt;/h2&gt;

&lt;p&gt;The moment a node can contain nodes, every subsystem gets a second opinion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dragging:&lt;/strong&gt; does moving the parent move the children? (Yes.) Does dragging a&lt;br&gt;
child near the edge leave the parent? (Your call — but it must be &lt;em&gt;a&lt;/em&gt; call, not an accident. One of the top-voted react-flow Stack Overflow questions is literally &lt;a href="https://stackoverflow.com/questions/72548536/react-flow-one-nodes-drags-other-nodes" rel="noopener noreferrer"&gt;"one node drags other nodes"&lt;/a&gt; — accidental containment.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hit-testing:&lt;/strong&gt; a click inside a container over empty space — who gets it?&lt;br&gt;
The container? The canvas? The deepest nested thing under the pointer?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serialization:&lt;/strong&gt; if membership isn't part of the document, a saved diagram&lt;br&gt;
reloads as loose boxes over a decorative rectangle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Undo:&lt;/strong&gt; dragging a node from one container into another touches two&lt;br&gt;
containers and a membership record. One gesture must be one undo step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Collapse:&lt;/strong&gt; fold a container and its links must re-anchor to the header —&lt;br&gt;
and unfold must restore &lt;em&gt;exactly&lt;/em&gt; what was there, even after a save/reload.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Libraries that treat groups as a rendering trick get the first demo right and the next four bullets wrong — which is exactly why the feature requests stay open for years. The correct-but-expensive answer is that containment has to live in the model.&lt;/p&gt;
&lt;h2&gt;
  
  
  Containment is membership
&lt;/h2&gt;

&lt;p&gt;In Grafloria a group is a model object with members, and everything else follows from that one decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GroupModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grafloria/engine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;diagram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GroupModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ingest-stage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ingest-stage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// membership is model state&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collapseGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ingest-stage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// reversible: a snapshot rides ON the group&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expandGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ingest-stage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// …so collapsed diagrams survive save/reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Members drag with the group. Collapse stores member positions and removed links&lt;br&gt;
&lt;em&gt;on the group itself&lt;/em&gt;, so a collapsed diagram serializes and expands correctly in&lt;br&gt;
a different session. Hit-testing is deepest-wins. And because group operations are commands, the cross-container drag really is one ⌘Z.&lt;/p&gt;
&lt;h2&gt;
  
  
  The dashboard version, since that's where nesting usually ends up
&lt;/h2&gt;

&lt;p&gt;The most common concrete ask behind "nested flows" is a dashboard section: a box of KPIs that lays out its own children and accepts tiles dragged in. In our dashboard kit that's one field on a widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kpis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;KPI section&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kpi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$6.8M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kpi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Customers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1,284&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drag a tile across the boundary and the other grid adopts it live; drag it back out and the board takes it back; either way one undo restores everything, and&lt;br&gt;
&lt;code&gt;toJSON()&lt;/code&gt; reports the tile under whichever parent it's actually in —&lt;br&gt;
derived from live membership, not from the array you authored. Resize a child taller than the section and the &lt;em&gt;section&lt;/em&gt; grows a row in its board rather than letting the child overflow.&lt;/p&gt;

&lt;p&gt;Try both live: &lt;a href="https://grafloria.com/demos/#grouping" rel="noopener noreferrer"&gt;group demos&lt;/a&gt; · &lt;a href="https://grafloria.com/demos/dashboard/nested-containers.html" rel="noopener noreferrer"&gt;dashboard containers&lt;/a&gt; · the concepts: &lt;a href="https://grafloria.com/learn/groups/" rel="noopener noreferrer"&gt;Groups&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — &lt;a href="https://grafloria.com/" rel="noopener noreferrer"&gt;grafloria.com&lt;/a&gt;. If this post was useful, the &lt;a href="https://grafloria.com/demos/" rel="noopener noreferrer"&gt;demo gallery&lt;/a&gt; is where the ideas live as running code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
