A client recently asked for something I didn't know a Kendo UI grid could do. They wanted both batch edit and a detail/expand edit of the same record in the grid.

They liked the Excel-like editing experience of working in batch mode, where you make a bunch of edits and send all the changes at once. But they also wanted several UI features in the expanded/detail view that don't make sense in a grid row (e.g., charts, hyperlinks, buttons to calculate things, etc.). The idea was that users could use whatever fit their needs or preference, then save all grid changes as usual in a batch.

I got it mostly working with this code, which is using this HTML and JavaScript: https://jsfiddle.net/joewilson0/jwcLhtg2/

<div id="myGrid"></div> 

<script type="text/x-kendo-template" id="myDetailTemplate">
  <div class="form-group">
    <label for="testNumber">Test Number</label>
    <input name="testNumber" class="form-control w-25" data-bind="value: testNumber" />
  </div>
  <div class="form-group">
    <label for="testString">Test String</label>
    <input name="testString" class="form-control w-25" data-bind="value: testString" />
  </div>
  <div class="form-group">
    <label for="testDate" class="d-block">Test Date</label>
    <input name="testDate" class="form-control" data-bind="value: testDate" data-role="datepicker" />
  </div>
</script>
$(document).ready(() => {
  const testData = [
    { testNumber: 123, testString: "apple", testDate: new Date(2020, 10, 14) },
    { testNumber: 456, testString: "pear", testDate: new Date(2020, 10, 15) },
    { testNumber: 789, testString: "plum", testDate: new Date(2020, 10, 16) },
  ];

  $("#myGrid").kendoGrid({
    dataSource: {
      data: testData,
      schema: {
        model: {
          fields: {
            testNumber: { type: "number" },
            testString: { type: "string" },
            testDate: { type: "date" }
          }
        }
      }
    },
    columns: [
      { field: "testNumber", title: "Test Number" },
      { field: "testString", title: "Test String" },
      { field: "testDate", title: "Test Date", format: "{0:d}" }
    ],
    editable: true,
    detailTemplate: kendo.template($("#myDetailTemplate").html()),
    detailInit: event => kendo.bind(event.detailRow, event.data)
  });
});

This line is the key to having the grid row and the expanded view share the same view model:

detailInit: event => kendo.bind(event.detailRow, event.data)

Now when you change a value in the grid row, it updates immediately in the expanded row. Great! But when you do the reverse, and update the expanded row value, the grid row updates, but the expanded row immediately collapses. Arg!

The fix to prevent closing the expanded row was to ignore the itemchange action in the dataBinding event:

function dataBinding(event) {
    // Prevent closing the expanded detail row when the itemchange action in the databinding event is fired so it doesn't close the expanded view
    if (event.action === "itemchange") {
        event.preventDefault();
    }
}

But this workaround fixes one thing and breaks another. Now the row doesn't collapse every time you change a value in the expanded view, but it also doesn't update the grid row values when you collapse because you just short-circuited that event.

To fix this new problem, I added a "Done" button to the template that collapses the expanded row and wired up a function to repaint the values in the grid row on collapse.

<button class="btn btn-secondary btn-collapse">Done</button>

If the user changes 3 things in the expanded view and keeps the expanded view open, the grid row still shows the original, unchanged values. Once the user clicks the "Done" button or collapses the expanded view with the icon on the left, the grid row is repainted with the values from the shared view model.

function repaintRow(row) {
    const grid = $(row).closest(".k-grid").data("kendoGrid");
    const dataItem = grid.dataItem(row);
    const rowChildren = $(row).children("td[role='gridcell']");

    for (let i = 0; i < grid.columns.length; i++) {
        const column = grid.columns[i];
        const template = column.template;
        const cell = rowChildren.eq(i);

        if (column.field !== undefined) {
            if (template !== undefined) {
                // Handle templated columns
                const kendoTemplate = kendo.template(template);
                cell.html(kendoTemplate(dataItem));
            } else {
                const fieldValue = dataItem[column.field];
                const format = column.format;
                const values = column.values;

                if (fieldValue && values) {
                    // Handle drop downs
                    for (let j = 0; j < values.length; j++) {
                        const value = values[j];
                        if (value.value === fieldValue.toString()) {
                            cell.html(value.text);
                            break;
                        }
                    }
                } else if (format !== undefined) {
                    // Handle formatted columns (like dates and currency)
                    cell.html(kendo.format(format, fieldValue));
                } else {
                    // Handle unformatted, untemplated, non-drop down values
                    cell.html(fieldValue);
                }
            }
        }
    }
}

Now we have to wire up the Kendo UI grid and button events:

$("#myGrid").kendoGrid({
     ...other grid settings...
    detailInit: detailInit,
    detailCollapse: detailCollapse,
    dataBinding: dataBinding,
    ...other grid settings...
});

function detailInit(event) {
    const grid = $(event.masterRow).closest(".k-grid").data("kendoGrid");
    const masterRow = event.masterRow;
    const detailRow = event.detailRow;

    // Bind the expanded view (detailRow) to the values in the master row
    kendo.bind(detailRow, event.data);    

    // Handle the detail row "Done" button click
    detailRow.find(".btn-collapse").click(() => grid.collapseRow(masterRow));
}

Here's the final working version that keeps the Kendo UI grid row and the expanded view in sync: https://jsfiddle.net/joewilson0/e3p70jac/2/