---
title: "Useful callbacks for DT (in Shiny)"
author: "Stéphane Laurent"
date: '2019-06-14'
tags: R, shiny, javascript, datatables
rbloggers: yes
output:
  md_document:
    variant: markdown
    preserve_yaml: true
    toc: yes
  html_document:
    highlight: kate
    keep_md: no
    toc: yes
prettify: yes
linenums: yes
prettifycss: minimal
highlighter: pandoc-solarized
---


# Edit cells on pressing Tab and arrow keys

This callback allows a more friendly way to edit the cells:

- navigate in the table, press 'Enter' to edit;

- press 'Enter' to validate the edit and stay at the same position;

- if you are editing a cell, then pressing 'Tab' or an arrow key will trigger 
the edit of the new cell.

This is done with the help of the `KeyTable` extension.

```r
library(shiny)
library(DT)

js <- c(
  "table.on('key', function(e, datatable, key, cell, originalEvent){",
  "  var targetName = originalEvent.target.localName;",
  "  if(key == 13 && targetName == 'body'){",
  "    $(cell.node()).trigger('dblclick.dt');",
  "  }",
  "});",
  "table.on('keydown', function(e){",
  "  var keys = [9,13,37,38,39,40];",
  "  if(e.target.localName == 'input' && keys.indexOf(e.keyCode) > -1){",
  "    $(e.target).trigger('blur');",
  "  }",
  "});",
  "table.on('key-focus', function(e, datatable, cell, originalEvent){",
  "  var targetName = originalEvent.target.localName;",
  "  var type = originalEvent.type;",
  "  if(type == 'keydown' && targetName == 'input'){",
  "    if([9,37,38,39,40].indexOf(originalEvent.keyCode) > -1){",
  "      $(cell.node()).trigger('dblclick.dt');",
  "    }",
  "  }",
  "});"
)

ui <- fluidPage(
  DTOutput("table")
)

server <- function(input, output, session){
  output[["table"]] <- renderDT({
    datatable(
      iris,
      selection = "none",
      editable = TRUE, 
      callback = JS(js),
      extensions = "KeyTable",
      options = list(
        keys = TRUE
      )
    )
  })
}

shinyApp(ui, server)
```

![](figures/DTcallback_editOnTab.gif)

# Select rows on click and drag

With this callback, which resorts to `jquery-ui`, you can select some rows 
on click and drag. You can also deselect all selected rows by double-clicking 
on the table.

```r
library(shiny)
library(DT)

callback <- c(
  "var dt = table.table().node();",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        table.row(i).select();",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});"
)

ui <- fluidPage(
  DTOutput("dt")
)

server <- function(input, output){
  output[["dt"]] <- renderDT({
    dtable <- datatable(
      iris, extensions = "Select", 
      callback = JS(callback), selection = "multiple"
    )
    dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
                                     "www/shared/jqueryui",
                                     script = "jquery-ui.min.js",
                                     package = "shiny")
    dtable$dependencies <- c(dtable$dependencies, list(dep))
    dtable
  })
}

shinyApp(ui, server)
```

![](figures/DTcallback_selectOnDrag.gif)

Unfortunately there is an issue: when you sort a column, the selected 
rows are lost. Below is another code which overcomes this issue; it uses 
a slightly different callback and the option `server = FALSE`.

```r
library(shiny)
library(DT)

callback <- c(
  "var dt = table.table().node();",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        table.row(':eq(' + i + ')').select();",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});"
)

ui <- fluidPage(
  DTOutput("dt")
)

server <- function(input, output){
  output[["dt"]] <- renderDT({
    dtable <- datatable(
      iris, extensions = "Select", 
      callback = JS(callback), selection = "multiple"
    )
    dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
                                     "www/shared/jqueryui",
                                     script = "jquery-ui.min.js",
                                     package = "shiny")
    dtable$dependencies <- c(dtable$dependencies, list(dep))
    dtable
  }, server = FALSE)
}

shinyApp(ui, server)
```

### Getting the selected rows

With the above code, `input[["dt_selected_rows"]]` provides only the 
rows selected by clicking, not the ones selected by dragging. 
Here is a code allowing to get both. 
The rows selected by clicking are given in `input[["dt_selected_rows"]]`, 
while the ones selected by dragging are given in `input[["dt_selected_rows2"]]`. 
There are some duplicates so we have to use `unique`. 

```r
library(shiny)
library(DT)

callback <- c(
  "function distinct(value, index, self){ 
    return self.indexOf(value) === index;
  }",
  "var dt = table.table().node();",
  "var tblID = $(dt).closest('.datatables').attr('id');",
  "var inputName = tblID + '_rows_selected2'",
  "var selected = [];",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        var row = table.row(':eq(' + i + ')')",
  "        row.select();",
  "        var rowIndex = parseInt(row.id().split('-')[1]);",
  "        selected.push(rowIndex);",
  "        selected = selected.filter(distinct);",
  "        Shiny.setInputValue(inputName, selected);",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});",
  "table.on('click', 'tr', function(){",
  "  var row = table.row(this);",
  "  if(!$(this).hasClass('selected')){",
  "    var rowIndex = parseInt(row.id().split('-')[1]);",
  "    var index = selected.indexOf(rowIndex);",
  "    if(index > -1){",
  "       selected.splice(index, 1);",
  "    }",
  "  }",
  "  Shiny.setInputValue(inputName, selected);",
  "});"
)

ui <- fluidPage(
  DTOutput("dt"),
  br(),
  verbatimTextOutput("selectedRows")
)

dat <- iris
dat$ROWID <- paste0("row-", 1:nrow(dat))

rowNames <- TRUE # whether to show row names in the table
colIndex <- as.integer(rowNames)

server <- function(input, output){
  output[["dt"]] <- renderDT({
    dtable <- datatable(
      dat, rownames = rowNames,  
      extensions = "Select", 
      callback = JS(callback),
      selection = "multiple", 
      options = list(
        rowId = JS(sprintf("function(data){return data[%d];}", 
                           ncol(dat)-1L+colIndex)),
        columnDefs = list( # hide the ROWID column
          list(visible = FALSE, targets = ncol(dat)-1L+colIndex)
        )
      )
    )
    dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
                                     "www/shared/jqueryui",
                                     script = "jquery-ui.min.js",
                                     package = "shiny")
    dtable$dependencies <- c(dtable$dependencies, list(dep))
    dtable
  }, server = FALSE)
  
  selectedRows <- reactive({
    unique(
      c(input[["dt_rows_selected"]], input[["dt_rows_selected2"]])
    )
  })
  
  output[["selectedRows"]] <- renderText({
    selectedRows()
  })
}

shinyApp(ui, server)
```



# Edit columns headers

This callback uses the `jQuery contextMenu` library. It allows to edit a 
column header by right-clicking on it. When done, press 'Escape' or move the 
mouse.

```r
library(shiny)
library(DT)

callback <- c(
  "$.contextMenu({",
  "  selector: '#table th',", 
  "  trigger: 'right',",
  "  autoHide: true,",
  "  items: {",
  "    text: {",
  "      name: 'Enter column header:',", 
  "      type: 'text',", 
  "      value: ''", 
  "    }",
  "  },",
  "  events: {",
  "    show: function(opts){",
  "      $.contextMenu.setInputValues(opts, {text: opts.$trigger.text()});",
  "    },",
  "    hide: function(opts){",
  "      var $this = this;",
  "      var data = $.contextMenu.getInputValues(opts, $this.data());",
  "      var $th = opts.$trigger;",
  "      $th.text(data.text);",
  "    }",
  "  }",
  "});" 
)

ui <- fluidPage(
  tags$head(
    tags$link(
      rel = "stylesheet", 
      href = "https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.8.0/jquery.contextMenu.min.css"
    ),
    tags$script(
      src = "https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.8.0/jquery.contextMenu.min.js"
    )
  ),
  DTOutput("table")
)

server <- function(input, output){
  output[["table"]] <- renderDT({
    datatable(iris, callback = JS(callback))
  }, server = FALSE)  
}

shinyApp(ui, server)
```

![](figures/DTcallback_editHeaders.png)


# Child tables

This callback allows to display child tables in the table. 
The indices of the selected rows of the child tables are sent to the 
Shiny server.

```r
library(shiny)
library(DT)
library(jsonlite)

## data ####
dat <- data.frame(
  Sr = c(1.5, 2.3),
  Description = c("A - B", "X - Y")
)
## details of row 1
subdat1 <- data.frame(
  Chromosome = "chr18", 
  SNP = "rs2",
  stringsAsFactors = FALSE
)
## details of row 2
subdat2 <- data.frame(
  Chromosome = c("chr19","chr20"), 
  SNP = c("rs3","rs4"), 
  stringsAsFactors = FALSE
)
## merge the row details
subdats <- lapply(list(subdat1, subdat2), purrr::transpose)
## dataframe for the datatable
Dat <- cbind(" " = "expand", dat, details = I(subdats))

## the callback ####
registerInputHandler("x.child", function(x, ...) {
  fromJSON(toJSON(x, auto_unbox = TRUE, null = "null"), 
           simplifyDataFrame = FALSE)
}, force = TRUE)

callback = JS(
  "var expandColumn = table.column(0).data()[0] === 'plus-sign' ? 0 : 1;",
  "table.column(expandColumn).nodes().to$().css({cursor: 'pointer'});",
  "",
  "// send selected columns of the main table to Shiny",
  "var tbl = table.table().node();",
  "var tblId = $(tbl).closest('.datatables').attr('id');",
  "var selector = 'td:not(:nth-child(' + (expandColumn+1) + '))';",
  "table.on('click', selector, function(){",
  "  setTimeout(function(){",
  "    var indexes = table.rows({selected:true}).indexes();",
  "    var indices = Array(indexes.length);",
  "    for(var i = 0; i < indices.length; ++i){",
  "      indices[i] = indexes[i];",
  "    }",
  "    Shiny.setInputValue(tblId + '_rows_selected', indices);",
  "  },0);",
  "});",
  "",
  "// make the table header of the nested table",
  "var format = function(d, childId){",
  "  if(d != null){",
  "    var html = '<table class=\"compact hover\" id=\"' + ", 
  "                childId + '\"><thead><tr>';",
  "    for(var key in d[d.length-1][0]){",
  "      html += '<th>' + key + '</th>';",
  "    }",
  "    html += '</tr></thead></table>'",
  "    return html;",
  "  } else {",
  "    return '';",
  "  }",
  "};",
  "",
  "// row callback to style the rows background colors of the child tables",
  "var rowCallback = function(row, dat, displayNum, index){",
  "  if($(row).hasClass('odd')){",
  "    $(row).css('background-color', 'papayawhip');",
  "    $(row).hover(function(){",
  "      $(this).css('background-color', '#E6FF99');",
  "    }, function() {",
  "      $(this).css('background-color', 'papayawhip');",
  "    });",
  "  } else {",
  "    $(row).css('background-color', 'lemonchiffon');",
  "    $(row).hover(function(){",
  "      $(this).css('background-color', '#DDFF75');",
  "    }, function() {",
  "      $(this).css('background-color', 'lemonchiffon');",
  "    });",
  "  }",
  "};",
  "",
  "// header callback to style the header of the child tables",
  "var headerCallback = function(thead, data, start, end, display){",
  "  $('th', thead).css({",
  "    'border-top': '3px solid indigo',", 
  "    'color': 'indigo',",
  "    'background-color': '#fadadd'",
  "  });",
  "};",
  "",
  "// make the child table",
  "var format_datatable = function(d, childId){",
  "  var dataset = [];",
  "  var n = d.length - 1;",
  "  for(var i = 0; i < d[n].length; i++){",
  "    var datarow = $.map(d[n][i], function(value, index){",
  "      return [value];",
  "    });",
  "    dataset.push(datarow);",
  "  }",
  "  var id = 'table#' + childId;",
  "  var subtable = $(id).DataTable({",
  "             'data': dataset,",
  "             'autoWidth': true,",
  "             'deferRender': true,",
  "             'info': false,",
  "             'lengthChange': false,",
  "             'ordering': d[n].length > 1,",
  "             'order': [],",
  "             'paging': false,",
  "             'scrollX': false,",
  "             'scrollY': false,",
  "             'searching': false,",
  "             'sortClasses': false,",
  "             'rowCallback': rowCallback,",
  "             'headerCallback': headerCallback,",
  "             'select': {style: 'multi'},",
  "             'columnDefs': [{targets: '_all', className: 'dt-center'}]",
  "           });",
  "};",
  "",
  "// send selected rows of the children tables to shiny server",
  "var nrows = table.rows().count();",
  "var nullinfo = Array(nrows);",
  "for(var i = 0; i < nrows; ++i){",
  "  nullinfo[i] = {row: i, selected: null};",
  "}",
  "Shiny.setInputValue(tblId + '_children:x.child', nullinfo);",
  "var sendToR = function(){",
  "  var info = [];",
  "  setTimeout(function(){",
  "    for(var i = 0; i < nrows; ++i){",
  "      var childId = 'child-' + i;",
  "      var childtbl = $('#'+childId).DataTable();",
  "      var indexes = childtbl.rows({selected:true}).indexes();",
  "      var indices;",
  "      if(indexes.length > 0){",
  "        indices = Array(indexes.length);",
  "        for(var j = 0; j < indices.length; ++j){",
  "          indices[j] = indexes[j];",
  "        }",
  "      } else {",
  "        indices = null;",
  "      }",
  "      info.push({row: i, selected: indices});",
  "    }",
  "    Shiny.setInputValue(tblId + '_children:x.child', info);",
  "  }, 0);",
  "}",
  "$('body').on('click', '[id^=child-] td', sendToR);",
  "",
  "// click event to show/hide the child tables",
  "table.on('click', 'td.details-control', function () {",
  "  var cell = table.cell(this);",
  "      row = table.row($(this).closest('tr'));",
  "  if(row.child.isShown()){",
  "    row.child.hide();",
  "    cell.data('expand');",
  "    sendToR();",
  "  } else {",
  "    var childId = 'child-' + row.index();",
  "    row.child(format(row.data(), childId)).show();",
  "    row.child.show();",
  "    cell.data('collapse-down');",
  "    format_datatable(row.data(), childId);",
  "  }",
  "});")

## render function, to display the glyphicons ####
render <- c(
  "function(data, type, row, meta){",
  "  if(type === 'display'){",
  "    return '<span style=\\\"color:black; font-size:18px\\\">' + ",
  "       '<i class=\\\"glyphicon glyphicon-' + data + '\\\"></i></span>';",
  "  } else {",
  "    return data;",
  "  }",
  "}"
)

## shiny app ####
ui <- fluidPage(
  DTOutput("table"),
  br(),
  fluidRow(
    column(6, 
           tags$label("Selected row(s) of main table:"),
           verbatimTextOutput("info-main")
    ),
    column(6, 
           tags$label("Selected row(s) of child tables:"),
           verbatimTextOutput("info-children")
    )
  )
)

server <- function(input, output){
  output[["table"]] <- renderDT({
    datatable(Dat, callback = callback, escape = -2, 
              extensions = "Select", selection = "none",
              options = list(
                select = list(style = "multi", selector = ".selectable"),
                autoWidth = FALSE,
                columnDefs = list(
                  list(className = "selectable dt-center", 
                       targets = c(0, 2:ncol(Dat))),
                  list(visible = FALSE, targets = ncol(Dat)),
                  list(orderable = FALSE, className = 'details-control', 
                       width = "10px", render = JS(render), targets = 1),
                  list(className = "dt-center", targets = "_all")
                )
              )
    )
  }, server = FALSE)
  
  output[["info-main"]] <- renderText({
    capture.output(input[["table_rows_selected"]])
  })
  
  output[["info-children"]] <- renderText({
    paste0(capture.output(input[["table_children"]]), collapse = "\n")
  })
  
}

shinyApp(ui, server)
```

![](figures/DTcallback_childTables.png)

# Change row CSS properties on clicking an icon

This callback allows to change the CSS properties of a row by clicking an icon. 
The indices of the altered rows are sent to the Shiny server.

```r
library(shiny)
library(DT)

rowNames <- TRUE # whether to show row names in the table
colIndex <- as.integer(rowNames)

callback <- c(
  sprintf("table.on('click', 'td:nth-child(%d)', function(){", colIndex+1),
  "  var td = this;",
  "  var cell = table.cell(td);",
  "  if(cell.data() === 'ok'){",
  "    cell.data('remove');",
  "  } else {",
  "    cell.data('ok');",
  "  }",
  "  var $row = $(td).closest('tr');",
  "  $row.toggleClass('excluded');",
  "  var excludedRows = [];",
  "  table.$('tr').each(function(i, row){",
  "    if($(this).hasClass('excluded')){",
  "      excludedRows.push(parseInt($(row).attr('id').split('_')[1]));",
  "    }",
  "  });",
  "  Shiny.setInputValue('excludedRows', excludedRows);",
  "})"
)

restore <- c(
  "function(e, table, node, config) {",
  "  table.$('tr').removeClass('excluded').each(function(){",
  sprintf("    var td = $(this).find('td').eq(%d)[0];", colIndex), 
  "    var cell = table.cell(td);", 
  "    cell.data('ok');",
  "  });",
  "  Shiny.setInputValue('excludedRows', null);",
  "}"
)

render <- c(
  'function(data, type, row, meta){',
  '  if(type === "display"){',
  '    var color = data === "ok" ? "forestgreen" : "red";',
  '    return "<span style=\\\"color:" + color +',
  '           "; font-size:18px\\\"><i class=\\\"glyphicon glyphicon-" +', 
  '           data + "\\\"></i></span>";',
  '  } else {',
  '    return data;',
  '  }',
  '}'
)

ui <- fluidPage(
  tags$head(
    tags$style(HTML(
      ".excluded { color: rgb(211,211,211); font-style: italic; }"
    ))
  ),
  fluidRow(
    column(
      6, 
      tags$label("Excluded rows"),
      verbatimTextOutput("excludedRows")
    ),
    column(
      6, 
      tags$label("Included rows"),
      verbatimTextOutput("includedRows")
    )
  ),
  br(),
  DTOutput("mytable")
)

server <- function(input, output,session) {
  
  dat <- cbind(Selected = "ok", mtcars[1:6,], id = paste0("row_",1:6))
  
  output[["mytable"]] <- renderDT({
    datatable(dat, rownames = rowNames, 
              extensions = c("Select", "Buttons"), 
              selection = "none", 
              callback = JS(callback),
              options = list(
                rowId = JS(sprintf("function(data){return data[%d];}", 
                                   ncol(dat)-1+colIndex)), 
                columnDefs = list(
                  list(visible = FALSE, targets = ncol(dat)-1+colIndex),
                  list(className = "dt-center", targets = "_all"),
                  list(className = "notselectable", targets = colIndex),
                  list(targets = colIndex, render = JS(render)) 
                ),
                dom = "Bt",
                buttons = list("copy", "csv",
                               list(
                                 extend = "collection",
                                 text = 'Select all rows', 
                                 action = JS(restore)
                               )
                ),
                select = list(style = "single", 
                              selector = "td:not(.notselectable)")
              )
    )
  }, server = FALSE)
    
    output$excludedRows <- renderPrint({
      input[["excludedRows"]]
    })

    output$includedRows <- renderPrint({
      setdiff(1:nrow(dat), input[["excludedRows"]])
    })
    
}

shinyApp(ui, server)
```

![](figures/DTcallback_rowCSSonclick.png)


# Capturing the autofilled cells

The `AutoFill` extension gives an Excel like option to a DataTable to click 
and drag over multiple cells, filling in information over the selected cells 
and incrementing numbers as needed.

The callback below allows to update the data in the R server when some 
cells are edited or changed by autofilling.

```r
library(shiny)
library(DT)

callback <- c(
  "var tbl = $(table.table().node());",
  "var id = tbl.closest('.datatables').attr('id');",
  "table.on('autoFill', function(e, datatable, cells){",
  "  var out = [];",
  "  for(var i = 0; i < cells.length; ++i){",
  "    var cells_i = cells[i];",
  "    for(var j = 0; j < cells_i.length; ++j){",
  "      var c = cells_i[j];",
  "      var value = c.set === null ? '' : c.set;", # null => problem in R
  "      out.push({",
  "        row: c.index.row + 1,",
  "        col: c.index.column,",
  "        value: value",
  "      });",
# to color the autofilled cells, uncomment the two lines below  
#  "      $(table.cell(c.index.row, c.index.column).node())",
#  "        .css('background-color', 'yellow');",
  "    }",
  "  }",
  "  Shiny.setInputValue(id + '_cells_filled:DT.cellInfo', out);",
  "  table.rows().invalidate();", # this updates the column type
  "});"
)

ui <- fluidPage(
  br(),
  DTOutput("dt"),
  br(),
  verbatimTextOutput("table")
)

server <- function(input, output){
  
  dat <- iris[1:5,]
  dat$Species <- as.character(dat$Species)
  
  output[["dt"]] <- renderDT({
    datatable(dat, 
              editable = list(target = "cell"),
              selection = "none",
              extensions = "AutoFill",
              callback = JS(callback), 
              options = list(
                autoFill = TRUE
              )
    )
  }, server = TRUE)
  
  Data <- reactive({
    info <- rbind(input[["dt_cells_filled"]], input[["dt_cell_edit"]])
    if(!is.null(info)){
      info <- unique(info)
      info$value[info$value==""] <- NA
      dat <<- editData(dat, info, proxy = "dt")
    }
    dat
  })

  output[["table"]] <- renderPrint({Data()})  
}

shinyApp(ui, server)
```

![](figures/DTcallback_AutoFill.gif)

If you use `server = FALSE` in `renderDT`, just remove the `proxy` argument 
in `editData`:

```r
dat <<- editData(dat, info)
```


# Select page with a numeric input


The default pagination is not convenient when there are many pages (the user 
has to click multiple times on the 'Next' or 'Previous' button). 
This callback allows to select a page with a numeric input.

```r
library(shiny)
library(DT)

shinyApp(
  ui = fluidPage(
    tags$head(tags$style(".pagination {float: right;}")),
    fluidRow(
      div(id="pagination", 
          div(style = "display:inline-block;", 
              tags$a(id = "first", style = "cursor: pointer;", "First")),
          div(style = "display:inline-block;", 
              tags$a(id = "previous", style = "cursor: pointer;", " Previous")),
          div(style = "display:inline-block;", 
              tags$input(id="page", type="number", class="input-sm", value="1", min="1")
          ),
          div(style = "display:inline-block;", 
              tags$span(id = "of")),
          div(style = "display:inline-block;", 
              tags$a(id = "next", style = "cursor: pointer;", "Next ")),
          div(style = "display:inline-block;", 
              tags$a(id = "last", style = "cursor: pointer;", "Last"))
      )
    ),
    fluidRow(
      column(12, DTOutput('tbl'))
    )
  ),
  server = function(input, output) {
    output$tbl = renderDT({
      datatable(
        iris, options = list(
          dom = "lfrti<'pagination'>", 
          initComplete = JS(c(
            "function(settings, json){",
            "  var table = settings.oInstance.api();",
            "  var pageinfo = table.page.info();",
            "  $('#of').text('of ' + pageinfo.pages);",
            "}"
          ))
        ),
        callback = JS(c(
          "$('div.pagination').append($('#pagination'));",
          "$('#first').on('click', function(){", 
          "  table.page('first').draw('page');",
          "  $('#page').val(1);",
          "});",
          "$('#previous').on('click', function(){", 
          "  table.page('previous').draw('page');",
          "  $('#page').val(table.page.info().page + 1);",
          "});",
          "$('#next').on('click', function(){", 
          "  table.page('next').draw('page');",
          "  $('#page').val(table.page.info().page + 1);",
          "});",
          "$('#last').on('click', function(){", 
          "  table.page('last').draw('page');",
          "  $('#page').val(table.page.info().pages);",
          "});",
          "$('#page').on('change', function(){",
          "  var page = parseInt($('#page').val());",
          "  if(!isNaN(page)){ table.page(page-1).draw('page'); }",
          "});"
        ))
      )
    })
  }
)
```

![](figures/DTcallback_pagination.gif)

