Creating a smart, dirty flag

http://www.knockmeout.net/2011/05/creating-smart-dirty-flag-in-knockoutjs.html

by andrew rowe

HTML

<script src="http://knockoutjs.com/downloads/knockout-3.2.0.js"></script>
<ul data-bind="template: { name: 'itemsTmpl', foreach: items }"></ul>

<script id="itemsTmpl" type="text/html">
  <li data-bind="css: { dirty: dirtyFlag.isDirty }">
    <span data-bind="text: id"></span>
    <input data-bind="value: name" />
    <button data-bind="click: dirtyFlag.reset">Reset</button>
  </li>
</script>
<button data-bind="enable: isDirty, click: save">Save</button>
<hr />
<h3>Just Dirty Items</h3>
<div data-bind="text: ko.toJSON(dirtyItems)"></div>

CSS

li {
  padding: 2px;
  margin: 2px;
}

input {
  width: 75px;
}

.dirty {
  border: solid yellow 2px;
}

JavaScript

ko.dirtyFlag = function(props) {
  var result = function() {},
    previous = [],
    _initialized = ko.observable(false);

  result.isDirty = ko.computed(function() {
    // grab all subscriptions just the first time through
    if (!_initialized()) {
      for (let i = 0; i < props.length; i++) {
        previous.push(ko.toJS(props[i]()));
      }
      _initialized(true);
      return false;
    }
    for (let i = 0; i < props.length; i++) {
      if (previous[i] != ko.toJS(props[i]()))
        return true;
    }
    return false;
  });

  result.reset = function() {
    previous = [];
    _initialized(false);
  };

  return result;
};


function Item(id, name) {
  this.id = ko.observable(id);
  this.name = ko.observable(name);
  this.dirtyFlag = new ko.dirtyFlag([this.id, this.name]);
}

var viewModel = {
  items: ko.observableArray([
    new Item(1, "one"),
    new Item(2, "two"),
    new Item(3, "three")
  ]),
  save: function() {
    alert("Sending changes to server: " + ko.toJSON(this.dirtyItems));
  }
};

viewModel.dirtyItems = ko.dependentObservable(function() {
  return ko.utils.arrayFilter(this.items(), function(item) {
    return item.dirtyFlag.isDirty();
  });
}, viewModel);

viewModel.isDirty = ko.dependentObservable(function() {
  return this.dirtyItems().length > 0;
}, viewModel);

ko.applyBindings(viewModel);