Configuring Ignition Perspective Table Drag and Drop

Karen Mitchell9 min read
HMI ProgrammingOther ManufacturerTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Ignition Perspective can show every task correctly while drag-and-drop still does nothing. That symptom points away from the task query, tag, driver, or controller and toward the browser event binding. The table rows need drag handlers, the drop target must accept the event, and the result must cross from browser JavaScript into a Perspective property before project logic can update the database.

What is the screen telling you?

Start with the exact operator symptom. A row that displays valid task data but cannot be dragged has already passed the data-display path. A row that drags but produces no assignment has a different failure: the browser started the operation, but the drop handler, Perspective property write, or database action did not complete.

Operator symptom Likely boundary First check
The row does not drag Rendered row lacks the HTML draggable attribute or ondragstart handler Inspect the rendered row and confirm both are present
The row drags, but the destination rejects it The target lacks an ondragover handler that calls preventDefault() Confirm the target accepts drag-over events
The drop occurs, but Perspective receives nothing The view lookup or view.custom.write call failed Watch view.custom.rowDroppedData
It works on page 1 but not page 2 Pagination replaced rendered row elements after handlers were attached Confirm a MutationObserver processes the new rows
Columns move while dragging a row Another table drag behavior is competing for the same pointer events Disable the competing drag or reorder option in the table configuration
The Perspective property changes, but the task remains unchanged Property-change or database logic rejected, skipped, or failed the update Log the two received IDs and the database result

If task data comes from a PLC-backed tag, a correctly displayed value only proves the display binding. This drag operation follows a separate path: rendered row, browser event, view custom property, validation, and database transaction. No controller write is required unless the application deliberately maps the assignment back into the control system.

Check: classify the failure by watching whether the row moves, whether the target accepts the drop, and whether the Perspective property changes.

Which identifiers cross the drag-and-drop boundary?

The table implementation places a row identifier in the rendered data-row-id attribute. The drag-start handler reads that attribute from the source row and stores it in the browser transfer object under text. The drop handler reads the transferred value as draggedId and reads the target row's own data-row-id as droppedId.

Value Location Effect
data-row-id Rendered table-row element Supplies the source or destination row identity
text Browser dataTransfer object Carries the source row ID during the drag
draggedId Drop result Identifies the task being moved
droppedId Drop result Identifies the row receiving the task
rowDroppedData View custom properties Hands both IDs to Perspective scripting

This payload describes row-to-row dropping. If containers B and C are separate Perspective containers rather than table rows, the supplied selector will not bind them because it selects only .tr-group.ia_table__rowGroup. Give each actual destination a stable destination identity, attach drop handlers to those elements, and pass that identity instead of a target row ID. Keep the task's database key separate from its displayed row position; sorting, filtering, and paging can change a visual position without changing the task identity.

Check: inspect one source and one target and confirm that each exposes the stable identity the database update requires.

Where should the drop event land in Perspective?

Add a custom property named rowDroppedData to the view. The injected script writes an object containing draggedId and droppedId to that property. A change script on the custom property can then validate the move and call the project or database logic.

A session property can also carry the result and may be easier for injected code to locate. Use the view custom property when the drag operation belongs to one view: it limits the event's scope and reduces collisions between open pages or repeated view instances. Use a session property only when the workflow intentionally spans views and the application supplies a way to distinguish concurrent events.

The supplied view-scoped method finds the view through window.__client.page.views._data, matches its mountPath, and calls view.custom.write. Those are browser-side implementation details rather than a stable component event contract. Treat the injection as a controlled workaround: test it after Perspective upgrades and whenever the view hierarchy changes.

The property is an event handoff, not proof that a database write succeeded. Repeatedly dropping the same pair may also write an object with values equal to the preceding event. Design the receiving script so a valid repeated action is either deliberately idempotent or carries application-level event state outside the injected identifiers.

Check: perform one drop and confirm view.custom.rowDroppedData changes to an object containing the expected source and target IDs.

How do you attach handlers to the rendered rows?

Bind a Markdown component so its source returns the hidden image and JavaScript below. The image's onload code locates the view, creates the three drag handlers, watches table-body mutations, and binds all currently rendered rows.

#make the propName the key to write too in the view.custom
propName = "rowDroppedData"
code =  """<img style='display:none' src='/favicon.ico' onload=\"
const view = [...window.__client.page.views._data.values()].find(view => view.value.mountPath == this.parentNode.parentNode.parentNode.getAttributeNode('data-component-path').value.split('.')[0]).value;
function ondrop(ev) {
ev.preventDefault();
const draggedId = ev.dataTransfer.getData('text');
const droppedId = this.getAttribute('data-row-id');
view.custom.write('"""+propName+"""',{'draggedId':draggedId,'droppedId':droppedId});
};
function ondragstart(ev){
ev.dataTransfer.setData('text', ev.target.getAttribute('data-row-id'));
};
function ondragover(ev){
ev.preventDefault();
};
const callbackTable = (mutationList, observer) => {
document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
e.setAttribute('draggable',true);
e.ondragstart = ondragstart;
e.ondragover = ondragover;
e.ondrop = ondrop;
});
};
const observerTable = new MutationObserver(callbackTable);
const tables = document.querySelectorAll('.ia_tableComponent .tb.ia_table__body');
tables.forEach(table => observerTable.observe(table, { attributes: false, childList: true, subtree: true }));
document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
e.setAttribute('draggable',true);
e.ondragstart = ondragstart;
e.ondragover = ondragover;
e.ondrop = ondrop;
});
\"></img>""".replace("\n", "").replace("\t", "")
return code

The initial querySelectorAll handles rows already on screen. The observer handles rows created later inside each observed table body. Calling preventDefault() in ondragover is what makes that row an eligible HTML drop target; omitting it commonly produces a drag cursor with no usable drop.

The selectors search the whole document. On a page with multiple tables, this code binds every matching row while writing through the view found from the Markdown component. Scope the selectors to the intended view or table when multiple instances exist, or one view can receive IDs from another table.

Check: inspect a rendered target row and verify draggable="true" plus assigned ondragstart, ondragover, and ondrop handlers.

How should the database assignment be committed?

Keep database access out of the injected browser code. The browser should report intent; trusted Perspective project logic should decide whether the move is legal and perform the update. Treat both received IDs as untrusted input even though they came from rendered rows.

  1. Reject a payload that does not contain both draggedId and droppedId.
  2. Resolve draggedId to the task's persistent key rather than relying on its current display index.
  3. Resolve droppedId to an allowed destination. For separate B and C containers, map the destination identity to the corresponding stored assignment.
  4. Check the user's authorization and any workflow rule that could prohibit the move.
  5. Update the task with a parameterized database operation inside a transaction appropriate to the application.
  6. Check the affected-row result. Treat zero affected rows as stale data, a missing task, or a rejected state transition rather than as success.
  7. Refresh or update the displayed task collections only after the database operation succeeds.

If two operators can move the same task, add an application concurrency check using the record state already maintained by the project. On rejection, restore the screen from the authoritative query and show a useful operator message. The exact table, query, and transaction identifiers must come from the project's data model; the drag script supplies only the two row IDs.

Check: confirm that one accepted drop changes exactly the intended task record and that a rejected destination changes no records.

Why does drag-and-drop fail after paging or table changes?

Perspective can replace browser row elements when the operator pages, sorts, filters, or when bound data refreshes. Event handlers attached to the old elements disappear with them. A MutationObserver addresses that lifecycle by running the binding callback when child elements change under an observed table body.

Page 2 is the decisive commissioning test. If its rows have no handlers, determine whether the existing observed body received new children or the complete table body was replaced. The supplied observer follows child changes in the body it originally observed; it cannot observe a replacement node after the old body is detached. Re-run the setup after that replacement or place the observation on a stable ancestor that survives page changes.

Column movement indicates competing table drag behavior. Locate the table setting responsible for column or header dragging and turn it off when row reassignment is the required gesture. No setting name is supplied here, so select it from the actual component configuration rather than copying a guessed property identifier.

Also test data refreshes. Each callback reassigns the same handler properties, so rebinding a surviving row is harmless, but newly generated rows must receive the attributes before the operator uses them.

Check: page forward, sort, filter, and refresh the table; then inspect a newly rendered row and complete another drop.

How do you verify the complete operator workflow?

  1. Open the view with known tasks in container A and known destinations in B or C.
  2. Record the task's persistent identity and current database assignment.
  3. Drag that task to one valid destination.
  4. Confirm the source identity and destination identity appear in view.custom.rowDroppedData.
  5. Confirm the property-change logic accepts the move and the database operation affects the intended record once.
  6. Refresh the view from the authoritative data source and verify the task appears only in its new container.
  7. Repeat after moving to page 2, sorting, filtering, and refreshing the bound data.
  8. Attempt a prohibited or invalid destination and verify that no database row changes.
  9. Open any other table or repeated view instance on the page and verify that its rows cannot write into the wrong view's property.

The final pass should test the operator-visible result after a fresh query, not merely the drag animation or custom-property value. That proves the rendered row, browser handlers, Perspective handoff, validation, database transaction, and refreshed display agree.

Frequently asked questions

How do I drag a row in an Ignition Perspective table?

Set the rendered row to draggable=true, copy its data-row-id into dataTransfer during ondragstart, and bind ondragover plus ondrop on the target.

How do I get the source and destination row IDs?

Read the source ID from ev.dataTransfer.getData('text') and the target ID from the dropped row's data-row-id. Write them as draggedId and droppedId to view.custom.rowDroppedData.

How do I keep Perspective drag-and-drop working on page 2?

Use a MutationObserver to bind newly rendered rows. If paging replaces the observed table body, attach the observer again or observe an ancestor that remains mounted.

How do I update the database after a row is dropped?

Run the update from the custom property's change logic, validate both IDs, authorize the destination, and use a parameterized transactional operation. Refresh the table only after the affected-row result confirms success.

How do I verify Perspective drag-and-drop end to end?

Drop a known task, verify rowDroppedData, confirm exactly one intended database record changed, refresh the query, and repeat the same final verification after paging, sorting, filtering, and a bound-data refresh.

Back to blog