From dcedadf24defe118eefc51f9f08fe8a232528cc3 Mon Sep 17 00:00:00 2001 From: agrath Date: Fri, 25 Oct 2013 16:59:48 +1300 Subject: [PATCH 1/2] Update ko.datasource.js to synchronise multiple invokes and extend to add ajax capabilities *Synchronise multiple invokes* When generator function is called, it is wrapped in a function which tracks how many times it has been invoked and only sets the observable if it is the last invoke. A new parameter is passed to the generator function, callback, which you should call with the value. Backwards compatibility is maintained so if you still set observable (bound to this) everything will still work. If you use the callback pattern, a new flag, async applies, as such: Original Syntax: self.data = ko.observable().extend({ datasource: self.getData }); New Syntax: self.data = ko.observable().extend({ datasource: { generator: self.getData, async: true } }); If async is true, then only the last result from calling generator will be applied. Therefore, you can still do whatever you want inside generator, such as just returning a local calculation or invoking a remote service and returning the result. If async is false, then every result from calling generator will be applied. Note: you must be using callback(value) inside your generator function if you wish to use async mode. *extend to add ajax capabilities* If you pass generator as a javascript object with options: url; the remote url method; the ajax method (get|post) params; parameters to pass to remote handler (unwrapped and subscribed) Then datasource will handle the ajax request internally. If async is true, multiple invokes will abort the previous xhr before issuing a new one. self.refresh = function(){ self.trigger(Math.random()) } self.trigger = ko.observable(Math.random()); self.data = ko.observable().extend({ datasource: { generator: { url: '/RemoteData.ashx', method: 'post', params: { q: 'test', trigger: self.trigger } }, async: true } }); Ajax functionality borrows parseJSON from jQuery and a modified version of tinyxhr from https://gist.github.com/shimondoodkin/4706967 --- ko.datasource.js | 206 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 169 insertions(+), 37 deletions(-) diff --git a/ko.datasource.js b/ko.datasource.js index cf552ca..de7ea8c 100644 --- a/ko.datasource.js +++ b/ko.datasource.js @@ -1,33 +1,136 @@ /*! https://github.com/CraigCav/ko.datasource */ -(function ( ko ) { - function datasource( source, target ) { +(function (ko) { + function datasource(source, target) { var _target = target || ko.observable(), paused = true, - trigger = ko.observable( false ), - loading = ko.observable( false ), - result = ko.computed( { + trigger = ko.observable(false), + loading = ko.observable(false), + result = ko.computed({ read: function () { - if ( paused ) { + if (paused) { paused = false; - trigger( true ); + trigger(true); } return _target(); }, - write: function ( newValue ) { - _target( newValue ); - loading( false ); + write: function (newValue) { + _target(newValue); + loading(false); }, deferEvaluation: true - } ); + }), + async = false, + callback = function (state, value) { + var source = state.source, isAsync = state.isAsync, sync = state.sync; + if (!isAsync) { + result(value); + } else { + if (source.sync == sync) { + result(value); + } + } + }, + curry = function (fn) { + var slice = [].slice, + args = slice.call(arguments, 1); + return function () { + return fn.apply(this, args.concat(slice.call(arguments))); + }; + }, + isFunction = function(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }, + //parseJSON borrowed from jQuery + parseJSON = function (data) { + if (!data) return null; + var rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g; + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + if ( typeof data === "string" ) { + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + if ( data ) { + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + } + } + } - ko.computed( function () { - if ( !trigger() ) return; - loading( true ); - source.call( result ); - } ); + jQuery.error( "Invalid JSON: " + data ); + }, + //tinyxhr by Shimon Doodkin - https://gist.github.com/4706967 + tinyxhr = function (url, callback, method, contentType, timeout, params) { + var requestTimeout,xhr; + try{ xhr = new XMLHttpRequest(); }catch(e){ + try{ xhr = new ActiveXObject("Msxml2.XMLHTTP"); }catch (e){ + if(console)console.log("tinyxhr: XMLHttpRequest not supported"); + return null; + } + } + var requestTimeout = setTimeout(function() {xhr.abort(); cb(new Error("tinyxhr: aborted by a timeout"), "",xhr); }, timeout || 10000); + xhr.onreadystatechange = function () { + if (xhr.readyState != 4) return; + clearTimeout(requestTimeout); + if (!xhr.aborted) { + callback(parseJSON(xhr.responseText), xhr.status != 200 ? new Error("tinyxhr: server response status is " + xhr.status) : false, xhr); + } + }; + xhr.open(method?method.toUpperCase():"GET", url, true); + if (!params) { + xhr.send(); + } + else { + xhr.setRequestHeader('Content-type', contentType ? contentType : 'application/x-www-form-urlencoded'); + xhr.send(params) + } + return xhr; + }, + query = function(settings, async, callback){ + var body = ko.computed(function () { + var serialized = JSON.stringify(settings.params, function (key, value) { + if (!ko.isObservable(value)) return value; + return ko.utils.unwrapObservable(value); + }); + return JSON.parse(serialized); + })(); + if (query.xhr && async) { + query.xhr.aborted = true; + query.xhr.abort(); + query.xhr = null; + } + query.xhr = tinyxhr(settings.url, callback, settings.method || 'POST', settings.contentType, settings.timeout, body); + }; + if (source.generator) { + async = source.async; + if (isFunction(source.generator)) { + source = source.generator; + } else { + source = curry(query, source.generator, async); + } + } + var generator = function () { + source.sync = (source.sync || 0) + 1 ; + var callbackInstance = curry(callback, { sync: source.sync, isAsync: async, source: source }); + source.call(result, callbackInstance); + }; + ko.computed(function () { + if (!trigger()) return; + loading(true); + generator.call(result); + }); result.refresh = function () { - trigger( trigger() + 1 ); + trigger(trigger() + 1); }; result.loading = loading; @@ -35,45 +138,74 @@ return result; } - function Pager( limit ) { - this.page = ko.observable( 1 ); - this.totalCount = ko.observable( 0 ); - this.limit = ko.observable( limit ); + function Pager(limit) { + this.page = ko.observable(1); + this.totalCount = ko.observable(0); + this.limit = ko.observable(limit); - this.totalPages = ko.computed( function () { - var count = Math.ceil( ko.utils.unwrapObservable( this.totalCount ) / ko.utils.unwrapObservable( this.limit ) ) + this.totalPages = ko.computed(function () { + var count = Math.ceil(ko.utils.unwrapObservable(this.totalCount) / ko.utils.unwrapObservable(this.limit)) return count == 0 ? 1 : count; - }, this ); + }, this); + + this.pages = ko.computed(function () { + var a = []; + var count = this.totalPages(); + for (var p = 0; p < count; p++) { + a.push(p + 1); + } + return a; + }, this); this.next = function () { var currentPage = this.page(); - this.page( currentPage + 1 ); - } .bind( this ); + this.page(currentPage + 1); + }.bind(this); this.previous = function () { var currentPage = this.page(); - this.page( currentPage === 0 ? 0 : currentPage - 1 ); - } .bind( this ); + this.page(currentPage === 0 ? 0 : currentPage - 1); + }.bind(this); + + this.specific = function (page) { + this.page(page); + }.bind(this); this.first = function () { - this.page( 1 ); - } .bind( this ); + this.page(1); + }.bind(this); this.last = function () { - this.page( this.totalPages() ); - } .bind( this ); + this.page(this.totalPages()); + }.bind(this); + + this.isFirstPage = ko.computed(function () { + return this.page() == 1; + }, this); + + this.isLastPage = ko.computed(function () { + return this.page() == this.totalPages(); + }, this); + + this.isNextPageAvailable = ko.computed(function () { + return !(this.isLastPage()); + }, this); + + this.isPrevPageAvailable = ko.computed(function () { + return !(this.isFirstPage()); + }, this); } - ko.extenders.datasource = function ( target, source ) { - var result = datasource( source, target ); + ko.extenders.datasource = function (target, source) { + var result = datasource(source, target); result.options = target.options || {}; return result; }; - ko.extenders.pager = function ( target, options ) { - var pager = new Pager( options.limit || 10 ); + ko.extenders.pager = function (target, options) { + var pager = new Pager(options.limit || 10); target.options = target.options || {}; target.options.pager = target.pager = pager; return target; }; -} )( ko ); \ No newline at end of file +})(ko); From dcc8cc3cb1dab0ba335edf63def681c3d848f649 Mon Sep 17 00:00:00 2001 From: agrath Date: Wed, 11 Dec 2013 17:06:54 +1300 Subject: [PATCH 2/2] Handle metadata for paging in response ajax object when using built in ajax mode --- ko.datasource.js | 79 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/ko.datasource.js b/ko.datasource.js index de7ea8c..6791fa2 100644 --- a/ko.datasource.js +++ b/ko.datasource.js @@ -21,12 +21,24 @@ }), async = false, callback = function (state, value) { - var source = state.source, isAsync = state.isAsync, sync = state.sync; - if (!isAsync) { - result(value); + var source = state.source, isAsync = state.isAsync, sync = state.sync + if (target.pager && value.meta) { + if (!isAsync) { + result(value.data); + result.pager.totalCount(value.meta.count); + } else { + if (source.sync == sync) { + result(value.data); + target.pager.totalCount(value.meta.count); + } + } } else { - if (source.sync == sync) { + if (!isAsync) { result(value); + } else { + if (source.sync == sync) { + result(value); + } } } }, @@ -91,24 +103,49 @@ } else { xhr.setRequestHeader('Content-type', contentType ? contentType : 'application/x-www-form-urlencoded'); + var serialize = function (obj) { + var str = []; + for (var p in obj) { + if (obj.hasOwnProperty(p)) { + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + } + } + return str.join("&"); + } + if (typeof (params) == 'object') { + params = serialize(params); + } xhr.send(params) } return xhr; }, query = function(settings, async, callback){ - var body = ko.computed(function () { - var serialized = JSON.stringify(settings.params, function (key, value) { - if (!ko.isObservable(value)) return value; - return ko.utils.unwrapObservable(value); - }); - return JSON.parse(serialized); - })(); + if (!query.params) { + query.params = ko.computed(function () { + if (settings.params) { + if (target.pager) { + var limit = target.pager.limit(); + var page = target.pager.page(); + settings.params.skip = (page-1) * limit; + settings.params.take = limit; + } + var serialized = JSON.stringify(settings.params, function (key, value) { + if (!ko.isObservable(value)) return value; + return ko.utils.unwrapObservable(value); + }); + + return JSON.parse(serialized); + } + return null; + }) + }; + if (query.xhr && async) { query.xhr.aborted = true; query.xhr.abort(); query.xhr = null; } - query.xhr = tinyxhr(settings.url, callback, settings.method || 'POST', settings.contentType, settings.timeout, body); + query.xhr = tinyxhr(settings.url, callback, settings.method || 'POST', settings.contentType, settings.timeout, query.params()); }; if (source.generator) { async = source.async; @@ -159,16 +196,28 @@ this.next = function () { var currentPage = this.page(); - this.page(currentPage + 1); + var next = currentPage + 1; + if (next > this.totalPages()) { + next = this.totalPages(); + } + this.page(next); }.bind(this); this.previous = function () { var currentPage = this.page(); - this.page(currentPage === 0 ? 0 : currentPage - 1); + var prev = currentPage === 0 ? 0 : currentPage - 1; + if (prev > this.totalPages()) { + prev = this.totalPages(); + } + if (prev > 0) { + this.page(prev); + } }.bind(this); this.specific = function (page) { - this.page(page); + if (page > 0 && page < this.totalPages()) { + this.page(page); + } }.bind(this); this.first = function () {