Use the grid control

Azure DevOps Services

This article shows different samples about the grid control.

Tip

For more information and examples, see the Formula Design System. See also the API Reference.

Grid height, grid width, and columns

This sample shows the basic usage of the grid. canSortBy is set to false for "Column 2" which disables sorting for that column.

  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");

  var container = $(".sample-container");

  var gridOptions: Grids.IGridOptions = {
    height: "100%",
    width: "100%",
    source: function () {
      var result = [], i;
      for (i = 0; i < 100; i++) {
        result[result.length] = [i, "Column 2 text" + i, "Column 3 " + Math.random()];
      }

      return result;
      } (),
      columns: [
        { text: "Column 1", index: 0, width: 50 },
        { text: "Column 2", index: 1, width: 200, canSortBy: false },
        { text: "Column 3", index: 2, width: 450 }]
  };

  Controls.create(Grids.Grid, container, gridOptions);

Update grid source

This sample shows how the grid can be updated after it's initialized using setDataSource.

  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");

  var container = $(".sample-container");

  var source = [
    { name: "Germany", population: 8e7 },
    { name: "Turkey", population: 75e6 },
    { name: "Russia", population: 15e7 },
    { name: "Spain", population: 45e6 }
  ];

  var gridOptions: Grids.IGridOptions = {
    height: "300px",
    width: "500px",
    source: source,
    columns: [
      { text: "Country", width: 200, index: "name" },
      { text: "Population", width: 200, index: "population" }
    ]
  };

  var grid = Controls.create(Grids.Grid, container, gridOptions);

  // Update source 5 seconds later
  window.setTimeout(() => {
    source.push({ name: "Belgium", population: 1e7 });
    source.push({ name: "France", population: 64e6 });
    grid.setDataSource(source);

  }, 5000);

Context menu

This sample shows the usage of contextMenu for each row in the grid. The item associated with the context menu is obtained using args.get_commandArgument().item in menuItemClick function.

  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");
  import Menus = require("VSS/Controls/Menus");

  var container = $(".sample-container");

  function menuItemClick(args) {
    // Get the item associated with the context menu
    var person = args.get_commandArgument().item;
    switch (args.get_commandName()) {
      case "open":
        alert(JSON.stringify(person));
        break;
      case "delete":
        confirm("Are you sure you want to delete " + person[0] + "?");
        break;
    }
  }

  function getContextMenuItems(): Menus.IMenuItemSpec[] {
    return [
      {
        id: "open",
        text: "Open Details"
      },
      { separator: true },
      {
        id: "delete",
        text: "Delete",
        icon: "icon-delete"
      },
    ];
  }

  var gridOptions: Grids.IGridOptions = {
    height: "300px",
    width: "500px",
    source: [
      ["Julie E. Baker", new Date(1946, 3, 24), "B+"],
      ["Patrica B. Allen", new Date(1991, 1, 28), "AB+"],
      ["Scott A. Reeves", new Date(1984, 1, 29), "0+"],
      ["Debra D. Carter", new Date(1962, 7, 2), "A+"],
      ["Shari A. Lefebvre", new Date(1951, 7, 25), "0+"]
    ],
    columns: [
      { text: "Name", width: 200 },
      { text: "Date of Birth", width: 100, format: "d" },
      { text: "Blood Type", width: 100 }
    ],
    gutter: {
      contextMenu: true
    },
    contextMenu: {
      items: getContextMenuItems(),
      executeAction: menuItemClick,
      arguments: (contextInfo) => {
        return { item: contextInfo.item };
      }
    }
  };

  Controls.create(Grids.Grid, container, gridOptions);

Open row details

This sample shows how an action can be executed when a row is opened. openRowDetail delegate is executed when the row is double-clicked or enter key is hit when a row has the focus.

  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");

  var container = $(".item-list");

  var gridOptions: Grids.IGridOptions = {
    height: "100%",
    width: "100%",
    source: [
      { fn: "Julie E. Baker", dob: new Date(1946, 3, 24), nationality: "US" },
      { fn: "Patrica B. Allen", dob: new Date(1991, 1, 28), nationality: "CA" },
      { fn: "Scott A. Reeves", dob: new Date(1984, 1, 29), nationality: "CA" },
      { fn: "Debra D. Carter", dob: new Date(1962, 7, 2), nationality: "CA" },
      { fn: "Shari A. Lefebvre", dob: new Date(1951, 7, 25), nationality: "US" },
      { fn: "Ahmet Yilmaz", dob: new Date(1965, 1, 2), nationality: "TR" },
      { fn: "Albert Einstein", dob: new Date(1879, 2, 14), nationality: "DE" },
      { fn: "Bastian Schweinsteiger", dob: new Date(1984, 7, 1), nationality: "DE" }
    ],
    columns: [
      { text: "Full Name", index: "fn", width: 200 },
      { text: "Date of Birth", index: "dob", width: 100, format: "d" }
    ],
    openRowDetail: (index: number) => {
      // Double clicking row or hitting enter key when the row is selected
      // will cause this function to be executed
      var person = grid.getRowData(index);

      // Display item details
      $(".item-details").text(JSON.stringify(person));
    }
  };

  var grid = Controls.create(Grids.Grid, container, gridOptions);

Hierarchy

By default first column is indented. Setting indent: true for a particular column causes that column to have indentation for hierarchy. See column options in the below sample. Settings collapsed: true get a parent item to be displayed collapsed. To update the data source, use gridSource.update(newItems);.

  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");

  var container = $(".sample-container");

  var gridItems: Grids.IGridHierarchyItem[] = [
    { id: "001", name: "Baking" },
    {
      id: "002", name: "Beverages", children: [
        { id: "003", name: "Coffee" },
        {
          id: "004", name: "Tea", collapsed: true, children: [
            { id: "005", name: "Green Tea" },
            { id: "006", name: "Black Tea" },
            { id: "007", name: "Herbal Tea" },
            { id: "008", name: "Fruit Tea" },
            { id: "009", name: "Decaffeinated" }
          ]
        },
        { id: "010", name: "Water" },
        { id: "011", name: "Hot Cocoa" },
        {
          id: "012", name: "Sports & Energy Drinks", children: [
            { id: "013", name: "Liquids" },
            { id: "014", name: "Energy" },
            { id: "015", name: "Specialty" },
            { id: "016", name: "Other" }
          ]
        },
        { id: "017", name: "Soft Drinks" }
      ]
    },
    { id: "018", name: "Frozen Foods" },
    { id: "019", name: "Candy" }
  ];

  var gridOptions: Grids.IGridOptions = {
    height: "600px",
    width: "450px",
    columns: [
      { text: "Id", index: "id", width: 60 },
      { text: "Product Name", index: "name", width: 200, indent: true }
    ]
  };

  var grid = Controls.create<Grids.Grid, Grids.IGridOptions>(Grids.Grid, container, gridOptions);
  var gridSource = new Grids.GridHierarchySource(gridItems);
  grid.setDataSource(gridSource);

Conditional formatting

This sample shows the customization of the grid cells. "Total" column is a calculated column and styled differently in getCellContents method.

  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");
  import Date_Utils = require("VSS/Utils/Date");
  import Number_Utils = require("VSS/Utils/Number");

  var container = $(".sample-container");

  function getColumns() {
    return [
      {
        index: "region",
        text: "Region",
        width: 80
      },
      {
        index: "rep",
        text: "Representative",
        width: 80
      },
      {
        index: "orderDate",
        text: "Order Date",
        width: 100,
        getCellContents: function (
          rowInfo, 
          dataIndex, 
          expandedState, 
          level, 
          column, 
          indentIndex, 
          columnOrder) {
            
          var orderDate = this.getColumnValue(dataIndex, column.index);
          return $("<div class='grid-cell'/>")
            .width(column.width || 100)
            .text(orderDate ? Date_Utils.localeFormat(orderDate, "y") : "");
        }
      },
      {
        index: "item",
        text: "Item",
        width: 80
      },
      {
        index: "unit",
        text: "Units",
        width: 60
      },
      {
        index: "cost",
        text: "Cost",
        width: 60,
        getCellContents: function (
          rowInfo, 
          dataIndex, 
          expandedState, 
          level, 
          column, 
          indentIndex, 
          columnOrder) {
          
          var cost = this.getColumnValue(dataIndex, "cost") || 0;
          return $("<div class='grid-cell'/>")
            .width(column.width || 150)
            .text(Number_Utils.localeFormat(cost, "C"));
        }
      },
      {
        index: "total",
        text: "Total",
        width: 150,
        getCellContents: function (
          rowInfo, 
          dataIndex, 
          expandedState, 
          level, 
          column, 
          indentIndex, 
          columnOrder) {
            
          var unit = this.getColumnValue(dataIndex, "unit") || 0,
            cost = this.getColumnValue(dataIndex, "cost") || 0,
            total = unit * cost;

          return $("<div class='grid-cell total'/>")
            .css("font-weight", "bold")
            .css("font-size", "11pt")
            .css("color", total >= 300 ? "red" : "green")
            .width(column.width || 100)
            .text(Number_Utils.localeFormat((unit * cost), "C"));
        },
        comparer: function (column, order, item1, item2) {
          var total1 = (item1.unit || 0) * (item1.cost || 0),
            total2 = (item2.unit || 0) * (item2.cost || 0);

          return total1 - total2;
        }
      }
    ];
  }

  function getSortOder() {
    return [{ index: "orderDate", order: "asc" }];
  }

  function getDataSource() {
    return [
      { orderDate: new Date(2010, 0, 6), region: 'Quebec', rep: 'Jones', item: 'Pencil', unit: 95, cost: 1.99 },
      { orderDate: new Date(2010, 0, 23), region: 'Ontario', rep: 'Kivell', item: 'Binder', unit: 50, cost: 19.99 },
      { orderDate: new Date(2010, 1, 9), region: 'Ontario', rep: 'Jardine', item: 'Pencil', unit: 36, cost: 4.99 },
      { orderDate: new Date(2010, 1, 26), region: 'Ontario', rep: 'Gill', item: 'Pen', unit: 27, cost: 19.99 },
      { orderDate: new Date(2010, 2, 15), region: 'Alberta', rep: 'Sorvino', item: 'Pencil', unit: 56, cost: 2.99 },
      { orderDate: new Date(2010, 3, 1), region: 'Quebec', rep: 'Jones', item: 'Binder', unit: 60, cost: 4.99 },
      { orderDate: new Date(2010, 3, 18), region: 'Ontario', rep: 'Andrews', item: 'Pencil', unit: 75, cost: 1.99 },
      { orderDate: new Date(2010, 4, 5), region: 'Ontario', rep: 'Jardine', item: 'Pencil', unit: 90, cost: 4.99 },
      { orderDate: new Date(2010, 4, 22), region: 'Alberta', rep: 'Thompson', item: 'Pencil', unit: 32, cost: 1.99 },
      { orderDate: new Date(2010, 5, 8), region: 'Quebec', rep: 'Jones', item: 'Binder', unit: 60, cost: 8.99 },
      { orderDate: new Date(2010, 5, 25), region: 'Ontario', rep: 'Morgan', item: 'Pencil', unit: 90, cost: 4.99 },
      { orderDate: new Date(2010, 6, 12), region: 'Quebec', rep: 'Howard', item: 'Binder', unit: 29, cost: 1.99 },
      { orderDate: new Date(2010, 6, 29), region: 'Quebec', rep: 'Parent', item: 'Binder', unit: 81, cost: 19.99 }
    ];
  }

  var gridOptions: Grids.IGridOptions = {
    width: "100%",
    height: "100%",
    columns: getColumns(),
    sortOrder: getSortOder(),
    source: getDataSource()
  };

  Controls.create(Grids.Grid, container, gridOptions);

Drag & drop within grid

This sample shows dragging grid rows and dropping them to the same grid, which basically changes the ordering of grid item.

  .row-drag-helper {
    background-color: orange;
    padding: 5px;
    cursor: move;
    padding-left: 30px;
  }
  
  .lower-drop-guide {
    border-bottom-color: orange;
  }
  
  .upper-drop-guide {
    border-top-color: orange;
  }
  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");
  
  var container = $(".sample-container");
  
  interface DragStartInfo extends Offset {
    dataIndex: number;
    canvasWidth: number;
  }
  
  interface DropTargetInfo {
    dataIndex: number;
    below: boolean;
  }
  
  interface MoveCompleteDelegate {
    (oldIndex: number, newIndex: number): void;
  }
  
  interface Offset {
    left: number;
    top: number;
  }
  
  class DragDropHandler {
    private _gridCanvas: JQuery;
    private _gridRowHeight: number;
    private _cursorOffset: Offset;
    private _dragStartInfo: DragStartInfo;
    private _lastDropTarget: JQuery;
  
    constructor(
      public grid: Grids.Grid,
      public dragdropScope: string,
      public dragdropTextProvider: (element: any) => string,
      public moveCompleteDelegate: MoveCompleteDelegate) {
  
      this.grid.setupDragDrop(this._getDraggableOptions(), this._getDroppableOptions());
      this._gridCanvas = this.grid.getElement().find(".grid-canvas");
      this._gridRowHeight = this.grid._rowHeight || 1;
    }
  
    private _resetLastDropTarget(): void {
      if (this._lastDropTarget) {
        this._lastDropTarget.removeClass("upper-drop-guide lower-drop-guide");
        this._lastDropTarget = null;
      }
    }
  
    private _getRowDataIndex(offset: Offset, dragStartInfo: DragStartInfo): DropTargetInfo {
      var canvasScrollTop = this._gridCanvas.scrollTop(),
        canvasScrollLeft = this._gridCanvas.scrollLeft(),
        offset = <Offset>{
          top: offset.top - dragStartInfo.top + canvasScrollTop + this._cursorOffset.top,
          left: offset.left - dragStartInfo.left + canvasScrollLeft + this._cursorOffset.left
        };
    
      // The left or right of the canvas, invalid drop
      if (offset.left <= 0 || offset.left > dragStartInfo.canvasWidth) {
        return null;
      }
  
      var itemsHeight = this.grid._count * this._gridRowHeight;
      // Above or below the canvas, invalid drop
      if (offset.top <= 0 || offset.top > itemsHeight) {
        return null;
      }
  
      var dataIndex = Math.floor((offset.top - 1) / this._gridRowHeight);
      // Dragged item and drop target is same, invalid drop
      if (dataIndex === dragStartInfo.dataIndex) {
        return null;
      }
    
      // Valid drop, return index of the drop target
      return {
        dataIndex: dataIndex,
        below: dataIndex > dragStartInfo.dataIndex
      };
  
    }
  
    private _getDraggableOptions(): any {
      this._cursorOffset = { left: 12, top: 12 };
  
      return {
        cursorAt: this._cursorOffset,
        axis: "",
        appendTo: document.body,
        scroll: false,
        scrollables: [".grid-canvas"],
        scrollablesAxis: "y",
        scope: this.dragdropScope,
        distance: 10,
        helper: (evt: JQueryEventObject, ui: any) => {
          var rowData = this.grid.getRowData(ui.draggingRowInfo.dataIndex);
          return $("<div />")
            .addClass("row-drag-helper")
            .text(this.dragdropTextProvider(rowData));
        },
        start: (evt: JQueryEventObject, ui: any) => {
          this._dragStartInfo = {
            top: ui.offset.top,
            left: ui.offset.left,
            dataIndex: ui.draggingRowInfo.dataIndex,
            canvasWidth: this._gridCanvas.width()
          };
        },
        stop: (evt: JQueryEventObject, ui: any) => {
          this._dragStartInfo = null;
          this._resetLastDropTarget();
        },
        drag: (evt: JQueryEventObject, ui: any) => {
          if (this._dragStartInfo) {
            this._resetLastDropTarget();
            var dropTargetInfo = this._getRowDataIndex(<Offset>ui.offset, this._dragStartInfo);
            if (dropTargetInfo) {
              this._lastDropTarget = <JQuery>this.grid.getRowInfo(dropTargetInfo.dataIndex).row;
              this._lastDropTarget.addClass(dropTargetInfo.below ? "lower-drop-guide" : "upper-drop-guide");
            }
          }
        }
      };
    }
  
    private _getDroppableOptions(): any {
      return {
        hoverClass: "",
        tolerance: "pointer",
        scope: this.dragdropScope,
        drop: (evt: JQueryEventObject, ui: any) => {
          var oldIndex = <number>ui.draggingRowInfo.dataIndex,
            newIndex = <number>ui.droppingRowInfo.dataIndex;
    
          // If different than selected index, perform move operation
          if (oldIndex !== newIndex && $.isFunction(this.moveCompleteDelegate)) {
            this.moveCompleteDelegate(oldIndex, newIndex);
          }
        }
      };
    }
  }
  
  var gridSource = function() {
    var result = [];
    for (var i = 0; i < 10; i++) {
      result.push([i, "Text " + i, Math.random()]);
    }
    return result;
  } ();
  
  var gridOptions: Grids.IGridOptions = {
    header: false,
    height: "300px",
    width: "500px",
    source: gridSource,
    columns: [
      { text: "Column 1", index: 0, width: 80 },
      { text: "Column 2", index: 1, width: 100 },
      { text: "Column 3", index: 2, width: 200 }]
  };
  
  var grid = Controls.create(Grids.Grid, container, gridOptions);
  var dragDrop = new DragDropHandler(grid, "my-scope", (item: any): string => {
    return `Moving item: ${JSON.stringify(item) }`;
  }, (oldIndex: number, newIndex) => {
    // Remove dragged item from the list
    var item = gridSource.splice(oldIndex, 1);
            
    // Add dragged item to the new location
    gridSource.splice(newIndex, 0, item[0]);
            
    // Update grid
    grid.setDataSource(gridSource);
            
    // Keep dropped item selected
    grid.setSelectedDataIndex(newIndex);
  });

Drag & drop outside of the grid

This sample shows dragging grid rows and dropping them to an outside target.

  <div class="drop-target"></div>
  .row-drag-helper {
    background-color: orange;
    padding: 5px;
    cursor: move;
    padding-left: 30px;
  }
  
  .drop-target {
    position: absolute;
    top:20px;
    right: 20px;
    border: 2px dashed blue;
    width: 300px;
    height: 200px;
    overflow: auto;
  }
  
  
  .drop-target::before {
    content: "drop here";
    position:absolute;
    right: 5px;
    top: 0;
    color: blue;
    font-size: 14pt;
    
  }
  
  .drop-target.active {
    background-color: lightgreen
  }
  import Controls = require("VSS/Controls");
  import Grids = require("VSS/Controls/Grids");
  
  var container = $(".sample-container");
  var scope = "my-scope";
  
  var gridOptions: Grids.IGridOptions = {
      header: false,
      height: "300px",
      width: "500px",
      source: function () {
      var result = [];
      for (var i = 0; i < 10; i++) {
        result.push([i, "Text " + i, Math.random()]);
      }
      return result;
    } (),
      columns: [
          { text: "Column 1", index: 0, width: 80 },
          { text: "Column 2", index: 1, width: 100 },
          { text: "Column 3", index: 2, width: 200 }],
  
    draggable: {
      cursorAt: { left: 12, top: 12 },
      axis: "",
      containment: "",
      appendTo: document.body,
      revert: "invalid",
      refreshPositions: true,
      scroll: false,
      scope: scope,
      distance: 10,
      helper: (evt: JQueryEventObject, ui: any) => {
        var rowData = grid.getRowData(ui.draggingRowInfo.dataIndex);
        var $helper = $("<div />")
          .addClass("row-drag-helper")
          .text(`Moving item: ${JSON.stringify(rowData[1]) }`);
  
        $helper.data("rowData", rowData);
        return $helper;
      }
    }
  };
  
  var grid = Controls.create(Grids.Grid, container, gridOptions);
  
  $(".drop-target").droppable({
    scope: scope,
    over: function (e, ui) {
      var $dropTarget = $(e.target);
      $dropTarget.addClass("active");
    },
    out: function (e, ui) {
      var $dropTarget = $(e.target);
      $dropTarget.removeClass("active");
    },
    drop: function (e, ui) {
      var $dropTarget = $(e.target);
      $dropTarget.removeClass("active");
  
      var $helper = ui.helper;
      var rowData = $helper.data("rowData");
      $("<div />").appendTo($dropTarget).text(JSON.stringify(rowData));
    }
  });