Angular+Handsontable

http://angularjs.org/

HTML

<div ng-controller="MyCtrl" id="x">
    <div id="thetable"></div>
    <pre>
{{pretty}}        
    </pre>
</div>
<div id="log"></div>

CSS

#x {
    float: left;
    width: 300px;
}

#log {
    border: 1px solid black;
    float: left;
    margin-left: 20px;
    width: 200px;
}

#log div:nth-child(even) {
    background-color: #ddf;
}

/**
 * It may be useful to copy the below styles and use on your page
 */
.handsontable {
  position: relative;
  font-family: Arial, Helvetica, sans-serif;
  line-height: 1.3em;
  font-size: 13px;
}

.handsontable * {
  box-sizing: content-box;
  -webkit-box-sizing: content-box;
  -moz-box-sizing: content-box;
}

.handsontable table {
  border-collapse: separate;
  position: relative;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  user-select: none;
  border-spacing: 0;
}

.handsontable th,
.handsontable td {
  border-right: 1px solid #CCC;
  border-bottom: 1px solid #CCC;
  min-width: 50px;
  height: 22px;
  line-height: 21px;
  padding: 0 4px 0 4px; /* top, bottom padding different than 0 is handled poorly by FF with HTML5 doctype */
}

.handsontable div.minWidthFix {
  width: 50px;
}

.handsontable tr:first-child th.htNoFrame,
.handsontable th:first-child.htNoFrame,
.handsontable th.htNoFrame {
  border-left-width: 0;
  background-color: white;
  border-color: #FFF;
}

.handsontable th:first-child,
.handsontable td:first-child,
.handsontable .htNoFrame + th,
.handsontable .htNoFrame + td {
  border-left: 1px solid #CCC;
}

.handsontable tr:first-child th,
.handsontable tr:first-child td {
  border-top: 1px solid #CCC;
}

.handsontable thead tr:last-child th {
  border-bottom-width: 0;
}

.handsontable thead tr.lastChild th {
  border-bottom-width: 0;
}

.handsontable th {
  background-color: #EEE;
  color: #222;
  text-align: center;
  font-weight: normal;
  white-space: nowrap;
}

.handsontable th .small {
  font-size: 12px;
}

.handsontable thead th {
  padding: 2px 4px;
}

.handsontable th.active {
  background-color: #CCC;
}

/* border background */
.handsontable .htBorderBg {
  position: absolute;
 ...

JavaScript

var myApp = angular.module('myApp',[]);

function log(msg) {
    var e = document.getElementById("log");
    e.innerHTML += "<div>" + msg + "</div>";
}

function MyObj(num, id, name, type, category) {
    this.num = num;
    this.id = id;
    this.name = name;
    this.type = type;
    this.category = category;
    if (category == null) {
        this.category = "NEW";
    }
}
MyObj.prototype.propUpdate = function(prop, value) {
    var match = value == this[prop];
    log("Updated property " + prop + " of " + this.num + ", values match: " + match);
};

function MyCtrl($scope) {
    var elem = $("#thetable");
    var newOne = { id: null, name: null, type: null, category: null };
    $scope.obj = [
        new MyObj(1, "a", "xyz", "type c", "P"),
        new MyObj(2, "b", "xya", "type c", "P"),
    ];
    elem.handsontable({
        startRows: 0,
        rowHeaders: true,
        minSpareRows: 1,
        colHeaders: ["ID", "Type", "Name", "Category"],
        columns: [
            { data: "id" },
            { data: "type" },
            { data: "name" },
            { data: "category" },
        ],
        data: $scope.obj,
        dataSchema: newOne,
        onChange: function(changes, source) {
            if (source == "loadData") return;
            var renderChanges = [];
            log(JSON.stringify(changes));
            for (var i = 0; i < changes.length; i++) {
                var idx = changes[i][0];
                var o = $scope.obj[idx];
                if (!o.propUpdate) {
                    o = new MyObj($scope.obj.length - 1, o.id, o.type, o.name, o.category);
                    $scope.obj[idx] = o;
                    renderChanges.push([idx, "id", null, o.id]);
                    renderChanges.push([idx, "type", null, o.type]);
                    renderChanges.push([idx, "name", null, o.name]);
                    renderChanges.push([idx, "category", null, o.category]);
                }
                o.propUpdate(changes[i][1],...