Lane based tasks with drag & drop

A lane based task manager supporting drag & drop.

In this example we build a lane based task manager supporting drag & drop and styled with Bootstrap 5.

The task manager consists of 3 lanes:

  • To do
  • Doing
  • Done

In the code below, lines 4 to 14 define the lane headers: a row with 3 columns "To do", "Doing" and "Done".

The first column also contains an "Add..." button. When pressed, the #newTodoModal modal dialog opens.

Then we have a row with the 3 dropzones: lines 16 to 45. The first lane already contains a task. The second lane (lines 35-39) a column with the ondrop-submit class. This tells us that the surrounding form will be submitted when an item is dropped on this columns. There is a formaction attribute that defines the URL to submit the form to. In addition, the ondrop-accept attribute defines that the 2nd column can only accept "todo" and "done" items.

The ondragover-addclass attribute will add the class "drop-zone" to the column when a dropover happens. This is only used for styling.

Lines 21 to 33 represent a task. It is draggable (has the draggable attribute set to true) and of type "todo" (determined by the drop-type attribute). It also has a drop-value of "1". This could be the Id of the task in a database.

As this task is of type "todo", it can be dropped over the "Doing" or the "Done" zone. If the task is dropped over the "Doing" zone, the surrounding form will be submitted to the following URL:

/TaskBoard/Drop?id=1&zone=1

The value of the drop-value attribute of the dropped item will substitute the special "{drop-value}" substitution field in the formaction URL. Then the whole form is submitted.

The server knows the item dropped and where it was dropped on. It also receives all form data. It can process the action and return new content. Since the form is inside a DIV element (line 1) that has a target class, new content for that DIV (a whole new form) will be returned by the server. This will allow the server to also recalculate the estimates in the footer columns (lines 47-51).

<div id="taskmanager" class="target">
  <form action="/TaskBoard" method="post" onkeyenter-click="">

    <div class="row">
      <div class="col alert alert-info">
        <b>To do</b>
        <button type="button" class="btn btn-primary btn-sm" onkeydown-click="+"
                data-bs-toggle="modal" data-bs-target="#newTodoModal">
          <i class="fas fa-plus"></i> Add...
        </button>
      </div>
      <div class="col alert alert-info"><b>Doing</b></div>
      <div class="col alert alert-info"><b>Done</b></div>
    </div>

    <div class="row">
      <div class="col ondrop-submit"
           ondrop-accept="doing done"
           formaction="/TaskBoard/Drop?id={drop-value}&zone=0"
           ondragover-addclass="drop-zone">
        <div class="card mb-3" draggable="true" drop-type="todo" drop-value="1">
          <div class="card-body">
            <span class="float-end">
              <button type="submit" class="btn btn-light btn-sm"
                  formaction="/TaskBoard/Remove?id=1"
                  onclick-confirm="Delete 'My First Task' ?">
                &times;
              </button>
            </span>
            <h5 class="card-title">My First Task</h5>
            <h6 class="card-subtitle mb-2 text-muted">Estimate: 8</h6>
          </div>
        </div>
      </div>
      <div class="col ondrop-submit"
         ondrop-accept="todo done"
         formaction="/TaskBoard/Drop?id={drop-value}&zone=1"
         ondragover-addclass="drop-zone">
      </div>
      <div class="col ondrop-submit"
         ondrop-accept="todo doing"
         formaction="/TaskBoard/Drop?id={drop-value}&zone=2"
         ondragover-addclass="drop-zone">
      </div>
    </div>

    <div class="row">
      <div class="col">Total estimate: 8</div>
      <div class="col">Total estimate: 0</div>
      <div class="col">Total estimate: 0</div>
    </div>

    <div class="modal fade onclose-restore" id="newTodoModal" tabindex="-1">
      <div class="modal-dialog target">
        <div class="modal-content" onkeyenter-click=">[type='submit'] :first">
          <div class="modal-header">
            <h5 class="modal-title">Add task</h5>
            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
          </div>
          <div class="modal-body">
            <div class="mb-3">
              <label class="form-label">Task label</label>
              <input type="text" class="form-control" name="NewTask.Label" autofocus>
            </div>
            <div class="mb-3">
              <label class="form-label">Estimation</label>
              <input type="number" class="form-control" name="NewTask.Estimate" value="0">
            </div>
          </div>
          <div class="modal-footer">
            <button type="submit" class="btn btn-primary" formaction="/TaskBoard/Add">
              <i class="spinner"></i> Add
            </button>
          </div>
        </div>
      </div>
    </div>

  </form>
<div>

When pressing the "Add..." button, the #newTodoModal defined on lines 53 to 77 opens. The modal is still inside the form and contains a submit button with a custom formaction (line 71). When submitted, the form is expected to return new content for its own content as there is a target class on line 54. This may seem strange but is meant to support server-side validation. If the data entered is not valid, the server will simply provide new content for the form including validation messages.

When new task data submitted is valid, the server will return a new version of the form (as it has to add the new task in place and recalculate the estimates). To tell the client this HTML replaces the form, the follwing response header is added by the server:

X-Sircl-Target: #taskmanager

#taskmanager identifies the DIV element surrounding the form and will have it's content replaced by the HTML code provided by the server.

Each task also bears a delete button (lines 24-28). This button also submits the form, but to a different URL: "/TaskBoard/Remove?id=1". When clicking the delete button, the user will be prompted for confirmation due to the onclick-confirm attribute. If accepted, the form is submitted and the server returns a new version of the form.

The task manager has also been optimized for keyboard usage. On the form, an onkeyenter-click attribute tells the form there is no button to be clicked when pressing the ENTER key. Pressing the + key however, triggers the "Add..." button as defined by the onkeydown-click attribute on line 7. And when pressing ENTER inside the modal, the first (and only) submit button is triggered due to the onkeyenter-click attribute onn line 55.

As a result, a user can add several tasks easily using only the keyboard by taking following actions:

Press the + key, enter the name of a task, press TAB, enter the estimate and press ENTER to save. Repeat for a new task.

The modal also has the class onclose-restore (line 53). When the modal dialog is opened and then closed (without saving), it's state is restored to avoid stale content the next time the modal dialog is opened.

With the following styling the drop zones are styled to have a minimum height and to show lightgreen during a drag & drop operation:

<style>
  .ondrop-submit {
    min-height: 64px;
  }
  .drop-zone {
    background-color: lightgreen;
  }
  .drop-zone * {
    pointer-events: none;
  }
</style>

 

Working demo
Loading demo...

 

Requirements

This example requires following Sircl library files to be loaded:

or:

Or their non-minified counterparts.

See the Get Started section about how to set up your project to use the Sircl library.