Skip to main content

DHTMLX Kanban. Guarding against duplicate or contradictory links example

The Kanban's link editor already prevents users from creating duplicate links. Already-linked cards are filtered out of the dropdown. However, when links are added programmatically via api.exec("add-link", ...), that UI filter is bypassed. The add-link handler only checks for duplicate IDs, so duplicate source/target/relation combinations can slip through. An interceptor can guard against this and block or report invalid programmatic additions.

Live example

const { Kanban } = kanban;
const { cards, columns, links } = getData();

const board = new Kanban("#root", {
    columns,
    cards,
    links,
    editorShape: [
        {
            type:"text",
            key: "label",
            label:"Label",
        },
        {
            type: "links",
            key: "links",
            label: "Links",
        },
    ],
});

function sameDirection(link, test){
    return test.source == link.source 
        && test.target == link.target;
}
function oppositeDirection(link, test){
    return test.target == link.source 
        && test.source == link.target;
}
function alreadyLinked(link) {
    const links = board.api.getState().links;
    return links.find(
        l =>
            (sameDirection(link, l) || oppositeDirection(link, l)) && link.relation === l.relation
    );
}

board.api.intercept("add-link", ({ link }) => {
    const exists = alreadyLinked(link);
    if (exists) {
        dhx.message({ 
            text: "These cards are already linked with a link of this type",
            css: "dhx_message--error",
            expire: 2000,
        });
        return false;
    }
});

function addIncorrectLink() {
    dhx.message({ 
        text: "Trying to add a link that already exists or mutually contradicts some of the links that already exist",
        expire: 2000,
    });

    board.api.exec("add-link", {
        link: links[0]
    })
}
<link rel="stylesheet" href="https://snippet.dhtmlx.com/codebase/assets/css/auxiliary_controls.css">
  
<!-- auxiliary controls for interacting with the sample -->  
<div class="g-wrap" style="height: 100%;">
    <div style="display: flex; flex-wrap: wrap; height: auto;">
        <button class="dhx_sample-btn dhx_sample-btn--cta" onclick="addIncorrectLink()">Add incorrect link</button>
    </div>
    <!-- component container -->
    <div id="root" style="height: 100%;"></div>
</div>


<!-- dataset  -->
<script src="https://snippet.dhtmlx.com/codebase/data/kanban/01/dataset.js"></script>

<style>
.dhx_sample-btn {
		margin: 4px;
	}
</style>

The code intercepts add-link events and checks the existing links via board.api.getState().links. The alreadyLinked function tests whether a link with the same source/target pair (or its reverse) and the same relation type already exists. If a duplicate is found, the interceptor returns false to block the link and displays an error message using dhx.message. A demo button triggers api.exec("add-link") with a known duplicate to demonstrate the validation.

Solution overview

  1. Use board.api.intercept("add-link", callback) to validate before a link is created
  2. Get current links from board.api.getState().links
  3. Check both same-direction and opposite-direction duplicates for the same relation type
  4. Return false from the interceptor to block the link
  5. Show feedback with dhx.message when a link is rejected

Key points

  • Check both directions: A→B and B→A with the same relation type are considered contradictory. Check oppositeDirection as well as sameDirection
  • relation matters: Two links between the same cards are valid if they have different relation types
  • api.getState().links: This returns the current links array, useful for any validation that needs to inspect existing data
  • Returning false from intercept: This silently cancels the action. Always pair it with user feedback (like dhx.message) so the user knows why their action was blocked

API reference

Additional resources