COOKBOOK / LIVE EXAMPLE
Kanban Board example with VSN.js
Real markup. Real VSN. Try it below.
← All examples · Open full screen ↗
View example source
<section id="kanban-app" class="example-card kanban">
<header class="kanban-header">
<div>
<h1>Kanban Board</h1>
<p class="kanban-subtitle">Plan, move, and finish work with a lightweight board.</p>
</div>
<div class="kanban-summary">
<div class="summary-block">
<span class="summary-label">Total</span>
<strong vsn-bind:from="total">0</strong>
</div>
<div class="summary-block">
<span class="summary-label">In Progress</span>
<strong vsn-bind:from="progressCount">0</strong>
</div>
<div class="summary-block">
<span class="summary-label">Done</span>
<strong vsn-bind:from="doneCount">0</strong>
</div>
</div>
</header>
<form class="kanban-form" vsn-on:submit!prevent="submit();">
<input
class="kanban-input"
placeholder="Add a new card to backlog"
autocomplete="off"
vsn-bind="newTitle"
vsn-on:keyup!enter="submit();"
/>
<select class="kanban-select" vsn-bind="newPriority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<button class="btn" type="submit">Add Card</button>
</form>
<div class="kanban-board">
<section class="kanban-column" data-status="backlog">
<header>
<h2>Backlog</h2>
<span class="pill" vsn-bind:from="backlogCount">0</span>
</header>
<ul class="kanban-list" vsn-html="backlogView"></ul>
</section>
<section class="kanban-column" data-status="progress">
<header>
<h2>In Progress</h2>
<span class="pill" vsn-bind:from="progressCount">0</span>
</header>
<ul class="kanban-list" vsn-html="progressView"></ul>
</section>
<section class="kanban-column" data-status="done">
<header>
<h2>Done</h2>
<span class="pill" vsn-bind:from="doneCount">0</span>
</header>
<ul class="kanban-list" vsn-html="doneView"></ul>
</section>
</div>
</section>
<script type="text/vsn">
use localStorage as storage;
use JSON as json;
use Date as date;
#kanban-app {
key: "vsn:kanban";
newTitle: "";
newPriority: "medium";
dragId: "";
dragFrom: "";
dropStatus: "";
tasks: [];
backlog: [];
progress: [];
done: [];
backlogView: "";
progressView: "";
doneView: "";
total: 0;
backlogCount: 0;
progressCount: 0;
doneCount: 0;
construct {
load();
}
async load() {
raw = storage.getItem(key);
tasks = raw ? json.parse(raw) : [];
await sync();
}
async save() {
storage.setItem(key, json.stringify(tasks));
}
async sync() {
backlog = await list.filter(tasks, (task) => task[2] == "backlog");
progress = await list.filter(tasks, (task) => task[2] == "progress");
done = await list.filter(tasks, (task) => task[2] == "done");
backlogView = renderCards(backlog, "Start", "progress", "Remove", "remove");
progressView = renderCards(progress, "Back", "backlog", "Finish", "done");
doneView = renderCards(done, "Reopen", "progress", "Remove", "remove");
backlogCount = backlog.length;
progressCount = progress.length;
doneCount = done.length;
total = tasks.length;
await save();
}
submit() {
title = newTitle.trim();
return title == "" ? null : addTask(title);
}
async addTask(title) {
id = "" + date.now();
tasks = tasks.concat([[id, title, "backlog", newPriority]]);
newTitle = "";
newPriority = "medium";
await sync();
}
async moveTask(id, status) {
updated = await list.map(tasks, (task) => {
return task[0] == id ? [task[0], task[1], status, task[3]] : task;
});
tasks = updated;
await sync();
}
async removeTask(id) {
tasks = await list.filter(tasks, (task) => task[0] != id);
await sync();
}
renderCards(list, primaryLabel, primaryTarget, secondaryLabel, secondaryTarget) {
cards = [];
for (task of list) {
id = task[0];
title = task[1];
status = task[2];
priority = task[3];
secondaryHtml = secondaryTarget == "remove"
? '<button class="ghost" data-id="' + id + '" vsn-on:click="removeTask(@data-id);">' + secondaryLabel + '</button>'
: '<button class="btn" data-id="' + id + '" data-target="' + secondaryTarget
+ '" vsn-on:click="moveTask(@data-id, @data-target);">' + secondaryLabel + '</button>';
card = html`
<li class="kanban-card" data-id="${id}" data-status="${status}" data-priority="${priority}" draggable="true">
<div class="card-head">
<span class="card-title">${title}</span>
<span class="priority">${priority}</span>
</div>
<div class="card-actions">
<button class="btn secondary" data-id="${id}" data-target="${primaryTarget}"
vsn-on:click="moveTask(@data-id, @data-target);">${primaryLabel}</button>
${secondaryHtml}
</div>
</li>
`;
cards = cards.concat([card]);
}
return html`${cards}`;
}
.kanban-card {
on dragstart(event) {
root.dragId = @data-id;
root.dragFrom = @data-status;
event.dataTransfer && event.dataTransfer.setData("text/plain", root.dragId);
}
on dragend() {
root.dragId = "";
root.dragFrom = "";
root.dropStatus = "";
}
}
.kanban-column {
@data-status :> status;
@class :< (root.dropStatus == status ? "kanban-column is-drop" : "kanban-column");
construct {
}
on dragover!prevent(event) {
root.dropStatus = status;
}
on drop!prevent(event) {
root.dropStatus = "";
root.dragId == "" ? null : moveTask(root.dragId, status);
}
}
}
</script>