VB: Dynamic List View with Drag and Drop

Steve Zebib
Oracle VB Studio and JET
6 min readAug 6, 2021

--

DISCLAIMER: The views expressed in this story are my own and do not necessarily reflect the views of Oracle.

Overview

This article provides an example for how to generate multiple list view components with drag and drop enabled. The drag and drop is useful for moving items between different list views.

For this example, we will build a drag and drop dashboard for moving tasks from one status to another. This will include 3 separate lists one for each of the following statuses: Open, In Progress, and Closed. This example will also generate the list views dynamically when the application starts.

Setup

NOTE: All of the below files including the JSON, HTML, and JS can be found in the VBCS web application main page flow.

JSON

  • The Metadata below includes all variables, types, action chains, and events required for this web application. Copy the following into the main-start-page.json file:
{
"title": "",
"description": "",
"variables": {
"dataReady": {
"type": "boolean",
"defaultValue": false
},
"treeDataProvider": {
"type": "any"
}
},
"metadata": {},
"types": {},
"chains": {
"initActionChain": {
"variables": {},
"root": "callFunctionBuildTree",
"actions": {
"callFunctionBuildTree": {
"module": "vb/action/builtin/callModuleFunctionAction",
"parameters": {
"module": "[[ $functions ]]",
"functionName": "buildTree",
"returnType": "any",
"params": []
},
"outcomes": {
"success": "assignVariablesTreeDataProvider"
}
},
"assignVariablesReady": {
"module": "vb/action/builtin/assignVariablesAction",
"parameters": {
"$page.variables.dataReady": {
"source": true
}
}
},
"assignVariablesTreeDataProvider": {
"module": "vb/action/builtin/assignVariablesAction",
"parameters": {
"$page.variables.treeDataProvider": {
"source": "{{ $chain.results.callFunctionBuildTree }}",
"reset": "empty"
}
},
"outcomes": {
"success": "assignVariablesReady"
}
}
}
}
},
"eventListeners": {
"vbEnter": {
"chains": [
{
"chainId": "initActionChain",
"parameters": {}
}
]
}
},
"imports": {
"components": {
"oj-input-date": {
"path": "ojs/ojdatetimepicker"
},
"oj-list-item-layout": {
"path": "ojs/ojlistitemlayout"
},
"oj-list-view": {
"path": "ojs/ojlistview"
}
}
},
"events": {}
}

HTML

  • The HTML below includes diagram component with its templates. Copy the following into main-start-page.html file:
<oj-bind-if test="[[ $variables.dataReady ]]">
<div id="container" class="oj-flex oj-sm-flex-wrap-nowrap">
<oj-bind-for-each data="[[ $variables.treeDataProvider ]]">
<template>
<div class="oj-flex oj-sm-flex-direction-column" style="border-right: 1px solid lightgrey;">
<div
class="oj-flex oj-sm-align-items-center oj-sm-align-self-center stage-header oj-sm-justify-content-space-between oj-sm-flex-wrap-nowrap">
<div class="oj-typography-body-md oj-typography-bold stage-header-text">
<oj-bind-text
value="[[ $current.data.statusName + ' (' + $functions.getCount($current.data.items) + ') ' ]]"></oj-bind-text>
</div>
</div>
<div class="oj-flex oj-sm-flex-direction-column oj-sm-flex-wrap-nowrap oj-sm-align-items-center">
<div class="oj-flex oj-sm-flex-direction-column">
<oj-list-view :id="[[ $current.data.statusId ]]"
data="[[$functions.buildADP($current.data.statusId,$current.data.items)]]"
dnd.drop.items.data-types='["application/ojlistviewitems+json"]'
dnd.drop.items.drop="[[$functions.handleDrop($current.data.statusId)]]"
dnd.drag.items.data-types='["application/ojlistviewitems+json"]'
dnd.drag.items.drag-start="[[$functions.handleDragStart($current.data.statusId)]]"
dnd.drag.items.drag-end="[[$functions.handleDragEnd($current.data.statusId)]]"
style="min-height: 500px; min-width: 250px;">
<template slot="itemTemplate" data-oj-as="item">
<li>
<oj-list-item-layout>
<div
class="oj-panel oj-flex oj-sm-flex-direction-column oj-sm-justify-content-space-between opty-card">
<div
class="oj-flex oj-sm-align-items-center oj-sm-justify-content-center">
<div role="button" tabindex="0"
class="oj-sm-align-self-center oj-sm-margin-4x-horizontal oj-listview-drag-handle"></div>
</div>
<div class="oj-flex" style="padding: 5px">
<div class="oj-text-color-primary">
<oj-bind-text value="[[ item.data.Name ]]"></oj-bind-text>
</div>
</div>
<div class="oj-flex-bar oj-sm-align-items-center"
style="height:40px; padding:5px">
<div class="oj-flex oj-sm-flex-direction-column oj-flex-bar-start">
<div
class="oj-flex-item oj-sm-12 oj-text-color-secondary oj-md-12">
<oj-input-date
value="[[ item.data.EffectiveDate ]]" readonly="true"
disabled="true"></oj-input-date>
</div>
</div>
<div class="oj-flex oj-sm-flex-direction-column oj-flex-bar-end">
<span class="oj-flex-item oj-sm-flex-initial vb-icon vb-icon-ellipsis"></span>
</div>
</div>
</div>
</oj-list-item-layout>
</li>
</template>
</oj-list-view>
</div>
</div>
</div>
</template>
</oj-bind-for-each>
</div>
</oj-bind-if>

JS

  • The JS below includes functions needed to initialize the diagram component. This includes functions which will be assigned to variables that are referenced by the component. Copy the following into main-start-page.js file:
define(["require", "knockout", "ojs/ojbootstrap", "ojs/ojarraytreedataprovider", "ojs/ojarraydataprovider", "ojs/ojknockout", "ojs/ojlistviewdnd"], function(
require, ko, ojbootstrap_1, ArrayTreeDataProvider, ArrayDataProvider) {
'use strict';
var treeDataArray;
var statusMap = new Map();
var dataTypes = "application/ojlistviewitems+json";
var dataArrayMap = new Map();
var handleDragStartMap = new Map();
var handleDragEndMap = new Map();
var handleDropMap = new Map();
var statusArray = [{
"Id": 1,
"Name": "Open"
},
{
"Id": 2,
"Name": "In Progress"
},
{
"Id": 3,
"Name": "Closed"
}
];
var dataArray = [{
"Name": "Task #1",
"Id": 10001,
"EffectiveDate": "2021-08-01T00:00:00.000+0000",
"StatusId": 3
},
{
"Name": "Task #2",
"Id": 10002,
"EffectiveDate": "2021-08-01T00:00:00.000+0000",
"StatusId": 3
},
{
"Name": "Task #3",
"Id": 10003,
"EffectiveDate": "2021-08-01T00:00:00.000+0000",
"StatusId": 2
},
{
"Name": "Task #4",
"Id": 10004,
"EffectiveDate": "2021-08-01T00:00:00.000+0000",
"StatusId": 2
},
{
"Name": "Task #5",
"Id": 10005,
"EffectiveDate": "2021-08-01T00:00:00.000+0000",
"StatusId": 1
}
];
var PageModule = function PageModule() {};PageModule.prototype.buildTree = function() {
for (let i = 0; i < statusArray.length; i++) {
let status = statusArray[i];
if (!statusMap.has(status.Id)) {
let data = {
"statusId": status.Id,
"statusName": status.Name,
"items": []
};
statusMap.set(status.Id, data);
}
};
treeDataArray = processArray(dataArray);return new ArrayTreeDataProvider(treeDataArray, {
keyAttributes: 'id',
keyAttributesScope: 'siblings',
childrenAttribute: 'items'
});
};
function processArray(array) {
if (array) {
for (let i = 0; i < array.length; i++) {
let item = array[i];
if (item) {
let data = statusMap.get(item.StatusId);
data.items.push(item);
data.count = data.items.length;
statusMap.set(item.StatusId, data);
}
};
};
let updatedArray = [...statusMap.values()];
updatedArray.sort((a, b) => (a.statusId > b.statusId) ? 1 : -1);
return ko.observableArray(updatedArray);
};
PageModule.prototype.getCount = function(array) {
let count = 0;
if (array) {
count = array.length;
}
return count;
};
PageModule.prototype.buildADP = function(statusId, array) {
this.dataArray = ko.observableArray(array);
dataArrayMap.set(statusId, this.dataArray);
let dataProvider = new ArrayDataProvider(this.dataArray, {
keyAttributes: 'Id'
});
return dataProvider;
};
PageModule.prototype.handleDragStart = function(statusId) {
// Handle drag start from drag and drop with mouse/touch.
let handleDragStart = (event, context) => {
const dataStr = event.dataTransfer.getData(dataTypes);
const data = JSON.parse(dataStr);
this.sourceItemId = context.items[0].Id;
this.sourceStatusId = context.items[0].StatusId;
};
handleDragStartMap.set(statusId, handleDragStart);
return handleDragStart;
};
PageModule.prototype.handleDragEnd = function(statusId) {
// Handle drag end from drag and drop with mouse/touch.
let handleDragEnd = (event) => {
if (event.dataTransfer.dropEffect !== "none") {}
};
handleDragEndMap.set(statusId, handleDragEnd);
return handleDragEnd;
};
PageModule.prototype.handleDrop = function(statusId) {
let handleDrop = (event, context) => {
event.preventDefault();
let index = -1;
if (context.item) {
const itemContext = document.getElementById(statusId).getContextByNode(context.item);
index = itemContext.index;
if (context.position === "after") {
index += 1;
}
}
handleDataTransfer(event.dataTransfer, index, statusId);
};
// Handle drop from drag and drop,
let handleDataTransfer = (dataTransfer, index, statusId) => {
const dataStr = dataTransfer.getData(dataTypes);
const data = JSON.parse(dataStr)[0];
if (this.sourceStatusId != statusId) {
insertTargetItem(data, index, statusId);
removeSourceItem(this.sourceItemId, this.sourceStatusId);
}
};
// Insert item into array. This is invoked by drop.
let insertTargetItem = (data, index, targetStatusId) => {
const arr = dataArrayMap.get(targetStatusId);
data.StatusId = targetStatusId;if (index === -1) {
arr.push(data);
} else {
arr.splice(index, 0, data);
}
arr.valueHasMutated();
treeDataArray.valueHasMutated();
};
// Remove item from array. This is invoked by drop.
let removeSourceItem = (itemId, sourceStatusId) => {
const arr = dataArrayMap.get(sourceStatusId);
if (arr) {
arr.remove(function(item) {
return item.Id === itemId;
});
arr.valueHasMutated();
treeDataArray.valueHasMutated();
}
};
handleDropMap.set(statusId, handleDrop);
return handleDrop;
};
return PageModule;
});

Test

  • Run the application and you should see the following:
  • Drag and drop an item from one status to another. For example, we will move Task #3 from ‘In Progress’ to ‘Closed’ status as shown below.

References

Oracle JET Cookbook — List View Drag and Drop

--

--