VSN.jsGitHub ↗

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>