function ScrollPosition(node) { this.node = node; this.previousScrollHeightMinusTop = 0; this.readyFor = 'up'; } ScrollPosition.prototype.restore = function () { if (this.readyFor === 'up') { this.node.scrollTop = this.node.scrollHeight - this.previousScrollHeightMinusTop; } // 'down' doesn't need to be special cased unless the // content was flowing upwards, which would only happen // if the container is position: absolute, bottom: 0 for // a Facebook messages effect } ScrollPosition.prototype.prepareFor = function (direction) { this.readyFor = direction || 'up'; this.previousScrollHeightMinusTop = this.node.scrollHeight - this.node.scrollTop; } function Model() { var self = this; var elViewport = document.querySelector('.viewport'); self.things = ko.observableArray([ randBackColor(), randBackColor(), randBackColor(), randBackColor(), randBackColor(), randBackColor(), randBackColor(), randBackColor(), randBackColor(), randBackColor()]); self.scrollPosition = new ScrollPosition(elViewport); self.scrollPosition.prepareFor('top'); self.unshift = function () { self.scrollPosition.prepareFor('up'); setTimeout(function () { self.things.unshift(randBackColor()); self.scrollPosition.restore(); }, 1000) } self.push = function () { self.scrollPosition.prepareFor('down'); setTimeout(function () { self.things.push(randBackColor()); self.scrollPosition.restore(); }, 1000) } function randBackColor() { return { backgroundColor: 'hsl( 0, 100%, ' + (Math.random() * 100) + '% )' }; } } var m = new Model(); ko.applyBindings(m);
<button data-bind="click: unshift">Unshift</button> <button data-bind="click: push">Push</button> <div class="viewport" data-bind="foreach: { data: things }"> <div data-bind="style: $data"></div> </div>
.viewport { height: 300px; overflow-y: scroll; border: 1px solid hsl(0, 100%, 5%); } .viewport div { height: 40px; }