Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

New $.dataSource() plug-in with accompanying sample.

  • Loading branch information...
commit f0c44e0e6d8414bdd739133f79374e4121d71d5b 1 parent 2e4aca3
Brad Olenick authored
View
146 grid-datamodel2/DataSourceSample.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Grid: Data</title>
+ <link rel="stylesheet" href="../themes/base/jquery.ui.all.css">
+ <link rel="stylesheet" href="grid.css">
+ <script src="../jquery-1.4.4.js"></script>
+ <script src="jquery.tmpl.js"></script>
+ <script src="jquery.tmplPlus.js"></script>
+ <script src="../ui/jquery.ui.core.js"></script>
+ <script src="../ui/jquery.ui.widget.js"></script>
+ <script src="grid.js"></script>
+ <script src="jquery.datalink2.js"></script>
+ <script src="jquery.datasource.js"></script>
+
+ <script>
+
+ var localDevelopers = [
+ {
+ firstName: "Scott",
+ lastName: "González",
+ country: "USA",
+ twitter: "scott_gonzalez",
+ github: "scottgonzalez"
+ },
+ {
+ firstName: "Richard",
+ lastName: "Worth",
+ country: "USA",
+ twitter: "rworth",
+ github: "rdworth"
+ },
+ {
+ firstName: "Jörn",
+ lastName: "Zaefferer",
+ country: "Germany",
+ twitter: "bassistance",
+ github: "jzaefferer"
+ }
+ ];
+
+ $(function() {
+ // We'll bind this array to a dataSource, which will control the array's contents.
+ var filteredLocalDevelopers = [];
+
+ // Bind a grid to filteredLocalDevelopers. The grid will react to array changes.
+ // Notice that the grid has no awareness of a dataSource.
+ // You can imagine a paging control that _would_ be aware of an ambient dataSource and would dynamically change query
+ // options (like we do in #filterDevelopers below) to change the contents of an array to which a grid is bound.
+ var developersGrid = $( "#developers-remote" ).grid({
+ columns: [ "firstName", "lastName", "country" ],
+ source: filteredLocalDevelopers
+ }).data( "grid" );
+ developersGrid.source = filteredLocalDevelopers; // TODO -- The widget factory deeply copies options, which loses filteredLocalDevelopers.
+
+ // The dataSource bound to filteredLocalDevelopers controls its contents.
+ // The "inputArray" option causes the dataSource to apply query options locally on refresh().
+ // We'll dynamically reconfigure the filter on this dataSource to change the contents of filteredLocalDevelopers.
+ $([ filteredLocalDevelopers ]).dataSource({
+ inputArray: localDevelopers,
+ refresh: function () { // With grid use data-linking to collections, this wouldn't be needed.
+ developersGrid.refresh();
+ }
+ }).refresh(); // refresh() causes the dataSource to update the contents of the array to which it's bound.
+
+ // Dynamically configure the query options on the dataSource. refresh() to update the array contents.
+ var filtered;
+ $("#filterDevelopers").click(function () {
+ if (filtered) {
+ $([ filteredLocalDevelopers ]).dataSource().option("filter").refresh(); // No r-value means unset.
+ } else {
+ $([ filteredLocalDevelopers ]).dataSource().option("filter", { property: "firstName", value: "Richard" }).refresh();
+ }
+ filtered = !filtered;
+ $("#filterDevelopers").val(filtered ? "Remove filter" : "Filter developers");
+ });
+
+ // A similar example...but with server integration and remote queries.
+ var genres = [];
+ var genresGrid = $( "#genres" ).grid({
+ columns: [ "Name" ],
+ source: genres
+ }).data( "grid" );
+ genresGrid.source = genres; // TODO -- The widget factory deeply copies options, which loses genres.
+
+ // No "inputArray" option means the dataSource should bind to remote data, employing options.urlMapper to render query options
+ // into a URL to $.ajax. I stole this urlMapper technique from Boris' earlier data model prototype.
+ $([ genres ]).dataSource($.extend({}, $.dataSource.oDataSettings, {
+ path: "http://odata.netflix.com/Catalog/Genres",
+ refresh: function () { // With grid use data-linking to collections, this wouldn't be needed.
+ genresGrid.refresh();
+ },
+ paging: { take: 10 }
+ })).refresh();
+
+ var sorted;
+ $("#sortGenres").click(function () {
+ if (sorted) {
+ $([ genres ]).dataSource().option("sort").refresh();
+ } else {
+ $([ genres ]).dataSource().option("sort", { property: "Name", direction: "desc" }).refresh();
+ }
+ sorted = !sorted;
+ $("#sortGenres").val(sorted ? "Remove sort" : "Sort genres");
+ });
+ });
+ </script>
+</head>
+<body>
+
+
+<h2>local data source</h2>
+<p>
+<table id="developers-remote">
+ <thead>
+ <tr>
+ <th data-field="firstName">First Name</th>
+ <th data-field="lastName">Last Name</th>
+ <th data-field="country">Country</th>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+</table>
+</p>
+
+<p><input id="filterDevelopers" type="button" value="Filter developers"></input></p>
+
+<h2>remote data source</h2>
+<p>
+<table id="genres">
+ <thead>
+ <tr>
+ <th data-field="Name">Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+</table>
+</p>
+
+<p><input id="sortGenres" type="button" value="Sort genres"></input></p>
+
+</body>
+</html>
View
3  grid-datamodel2/grid.js
@@ -14,7 +14,8 @@ $.widget( "ui.grid", {
options: {
columns: null,
type: null,
- rowTemplate: null
+ rowTemplate: null,
+ editable: []
},
_create: function() {
var that = this;
View
414 grid-datamodel2/jquery.datasource.js
@@ -0,0 +1,414 @@
+(function ($) {
+
+ $.fn.extend({
+ dataSource: function (options) {
+ // TODO -- I pick off the first element here, following a pattern in jquery.validate.js.
+ // Is there a preferred pattern here?
+ return jQuery.dataSource(this[0], options);
+ }
+ });
+
+ $.dataSource = function (targetArray, options) {
+ if (options) {
+ var currentDataSource = targetArray.__dataSource__;
+ if (currentDataSource) {
+ currentDataSource.destroy();
+ }
+
+ options = $.extend({}, options, { targetArray: targetArray });
+
+ var dataSource;
+ if (options.factory) {
+ // For extensibility, defer to a factory function that will produce a dataSource
+ // instance implementing the dataSource API.
+ dataSource = options.factory(options);
+ } else {
+ dataSource = options.inputArray ? new LocalDataSource(options.inputArray, options) : new RemoteDataSource(options);
+ // TODO -- dataSource should be a function so its not serializable.
+ }
+
+ targetArray.__dataSource__ = dataSource;
+ }
+
+ return targetArray.__dataSource__;
+ };
+
+ $.dataSource.oDataSettings = {
+ resultsFilter: function (data) {
+ this.totalCount = data.d.__count;
+ return data.d.results;
+ },
+
+ urlMapper: function (path, queryParams, sortProperty, sortDir, filter, skip, take, includeTotalCount) {
+ var questionMark = (path.indexOf("?") < 0 ? "?" : "&");
+ for (param in queryParams) {
+ path = path.split("$" + queryParam).join(queryParams[param]);
+ }
+ path += questionMark + "$format=json" +
+ // TODO -- Without inlineCount, the form of the AJAX result changes and resultsFilter breaks.
+ // (includeTotalCount ? "&$inlinecount=allpages" : "") +
+ "&$inlinecount=allpages" +
+ "&$skip=" + (skip || 0) +
+ (take !== null && take !== undefined ? ("&$top=" + take) : "");
+
+ if (sortProperty) {
+ path += "&$orderby=" + sortProperty + (sortDir && sortDir.toLowerCase().indexOf("desc") === 0 ? "%20desc" : "");
+ }
+ if (filter) {
+ filter = Object.prototype.toString.call(filter) === "[object Array]" ? filter : [ filter ];
+ $.each(filter, function (index, filterPart) {
+ path +=
+ "&$filter=" + filterPart.filterProperty + filterPart.filterOperator +
+ (typeof filterPart.filterBy === "string" ? ("'" + filterPart.filterBy + "'") : filterPart.filterBy);
+ });
+ }
+ return path;
+ }
+ }
+
+ function DataSource (options) {
+ if (!options) {
+ return;
+ }
+
+ this.itemsArray = options.targetArray || [];
+ // TODO -- Bind to the "arrayChange" event on itemsArray. LocalDataSource subclass would translate adds/removes
+ // onto its inputItemsArray. RemoteDataSource would commit the adds/removes directly or accumulate a changelist.
+
+ this._applyOptions(options);
+ };
+
+ DataSource.prototype = {
+ _refreshingHandler: null,
+ _refreshHandler: null,
+
+ _sortProperty: null, // TODO -- Generalize these to [ { property: ..., direction: ... } ].
+ _sortDir: null,
+ _filter: null,
+ _skip: null,
+ _take: null,
+ _includeTotalCount: false,
+
+ itemsArray: [],
+ totalCount: 0,
+
+ destroy: function () {
+ if (this.itemsArray) {
+ delete this.itemsArray.__dataSource__;
+ this.itemsArray = null;
+ }
+ },
+
+ option: function (option, value) {
+ this._applyOption(option, value);
+ return this;
+ },
+
+ options: function (options) {
+ this._applyOptions(options);
+ return this;
+ },
+
+ refresh: function (options) {
+ if (options) {
+ var hasDataSourceOptions;
+ for (optionName in options) {
+ if (optionName !== "all") {
+ hasDataSourceOptions = true;
+ break;
+ }
+ }
+ if (hasDataSourceOptions) {
+ // _applyOptions trounces all the old query options, so only call if we really have options here.
+ this._applyOptions(options);
+ }
+ }
+ if (this._refreshingHandler) {
+ this._refreshingHandler();
+ }
+ var self = this;
+ this._refresh(options, function () {
+ if (self._refreshHandler) {
+ self._refreshHandler();
+ }
+ });
+ return this;
+ },
+
+ _applyOption: function (option, value) {
+ switch (option) {
+ case "filter":
+ this._setFilter(value);
+ break;
+
+ case "sort":
+ this._setSort(value);
+ break;
+
+ case "paging":
+ this._setPaging(value);
+ break;
+
+ case "refreshing":
+ this._refreshingHandler = value;
+ break;
+
+ case "refresh":
+ this._refreshHandler = value;
+ break;
+
+ default:
+ throw "Unrecognized option '" + option + "'";
+ }
+ },
+
+ // N.B. Null/undefined option values will unset the given option.
+ _applyOptions: function (options) {
+ options = options || {};
+
+ var self = this;
+ $.each([ "filter", "sort", "paging", "refreshing", "refresh" ], function (index, optionName) {
+ self._applyOption(optionName, options[optionName]);
+ });
+ },
+
+ _processFilter: function (filter) {
+ var filterProperty = filter.property,
+ filterValue = filter.value,
+ filterOperator;
+ if (!filter.operator) {
+ filterOperator = "==";
+ } else {
+ var operatorStrings = {
+ "<": ["<", "islessthan", "lessthan", "less", "lt"],
+ "<=": ["<=", "islessthanorequalto", "lessthanequal", "lte"],
+ "==": ["==", "isequalto", "equals", "equalto", "equal", "eq"],
+ "!=": ["!=", "isnotequalto", "notequals", "notequalto", "notequal", "neq", "not"],
+ ">=": [">=", "isgreaterthanorequalto", "greaterthanequal", "gte"],
+ ">": [">", "isgreaterthan", "greaterthan", "greater", "gt"]
+ },
+ lowerOperator = filter.operator.toLowerCase();
+ for (op in operatorStrings) {
+ if ($.inArray(lowerOperator, operatorStrings[op]) > -1) {
+ filterOperator = op;
+ break;
+ }
+ }
+
+ if (!filterOperator) {
+ throw "Unrecognized filter operator '" + filter.operator + "'.";
+ }
+ }
+
+ return {
+ filterProperty: filterProperty,
+ filterOperator: filterOperator,
+ filterValue: filterValue
+ };
+ },
+
+ _refresh: function (options, completed) {
+ throw "'_refresh' is a pure virtual function";
+ },
+
+ _setFilter: function (filter) {
+ throw "'_setFilter' is a pure virtual function";
+ },
+
+ _setPaging: function (options) {
+ options = options || {};
+ this._skip = options.skip;
+ this._take = options.take;
+ this._includeTotalCount = !!options.includeTotalCount;
+ },
+
+ _setSort: function (options) {
+ options = options || {};
+ this._sortProperty = options.property;
+ this._sortDir = options.direction;
+ }
+ };
+
+ function LocalDataSource (inputArray, options) {
+ DataSource.apply(this, [ options ]);
+
+ this._inputItemsArray = inputArray;
+ // TODO -- You can imagine binding to "arrayChange" on inputArray and raising an event when the data source
+ // encounters changes that would require the client to "refresh" to reapply the query to the input data.
+ // This would keep the app author the flexibility to control when they reapply local queries (so items don't
+ // disappear when edited) without coupling their edit/refresh logic.
+ };
+
+ LocalDataSource.prototype = $.extend({}, new DataSource(), {
+ _inputItemsArray: [],
+
+ _applyQuery: function () {
+ var items = this._inputItemsArray;
+ var self = this;
+
+ var filteredItems;
+ if (this._filter) {
+ filteredItems = $.grep(items, function (item, index) {
+ return self._filter(item);
+ });
+ } else {
+ filteredItems = items;
+ }
+
+ var sortedItems;
+ if (this._sortProperty) {
+ var isAscending = (this._sortDir || "asc").toLowerCase().indexOf("asc") === 0;
+ sortedItems = filteredItems.sort(function (item1, item2) {
+ var propertyValue1 = self._normalizePropertyValue(item1, self._sortProperty),
+ propertyValue2 = self._normalizePropertyValue(item2, self._sortProperty);
+ if (propertyValue1 == propertyValue2) {
+ return 0;
+ } else if (propertyValue1 > propertyValue2) {
+ return isAscending ? 1 : -1;
+ } else {
+ return isAscending ? -1 : 1;
+ }
+ });
+ } else {
+ sortedItems = filteredItems;
+ }
+
+ var skip = this._skip || 0,
+ pagedItems = sortedItems.slice(skip);
+ if (this._take) {
+ pagedItems = pagedItems.slice(0, this._take);
+ }
+ var totalCount = this._includeTotalCount ? sortedItems.length : undefined;
+
+ return { items: pagedItems, totalCount: totalCount };
+ },
+
+ _createFilterFunction: function (filter) {
+ var self = this;
+ if (Object.prototype.toString.call(filter) === "[object Array]") {
+ var comparisonFunctions = $.map(filter, function (subfilter) {
+ return createFunction(subfilter);
+ });
+ return function (item) {
+ for (var i = 0; i < comparisonFunctions.length; i++) {
+ if (!comparisonFunctions[i](item)) {
+ return false;
+ }
+ }
+ return true;
+ };
+ } else {
+ return createFunction(filter);
+ }
+
+ function createFunction (filter) {
+ var processedFilter = self._processFilter(filter),
+ filterProperty = processedFilter.filterProperty,
+ filterOperator = processedFilter.filterOperator,
+ filterValue = processedFilter.filterValue;
+
+ var comparer;
+ switch (filterOperator) {
+ case "<": comparer = function (propertyValue) { return propertyValue < filterValue; }; break;
+ case "<=": comparer = function (propertyValue) { return propertyValue <= filterValue; }; break;
+ case "==": comparer = function (propertyValue) { return propertyValue == filterValue; }; break;
+ case "!=": comparer = function (propertyValue) { return propertyValue != filterValue; }; break;
+ case ">=": comparer = function (propertyValue) { return propertyValue >= filterValue; }; break;
+ case ">": comparer = function (propertyValue) { return propertyValue > filterValue; }; break;
+ default: throw "Unrecognized filter operator.";
+ };
+
+ return function (item) {
+ // Can't trust added items, for instance, to have all required property values.
+ var propertyValue = self._normalizePropertyValue(item, filterProperty);
+ return comparer(propertyValue);
+ };
+ };
+ },
+
+ _normalizePropertyValue: function (item, property) {
+ return item[property] || "";
+ },
+
+ _refresh: function (options, completed) {
+ var self = this;
+ if (options && !!options.all) {
+ var inputDataSource = $([ this._inputItemsArray ]).dataSource();
+ if (inputDataSource) {
+ // If the input array is bound to a data source, refresh it as well.
+ inputDataSource.refresh({
+ all: true,
+ completed: function () {
+ completeRefresh();
+ }
+ });
+ }
+ } else {
+ completeRefresh();
+ }
+
+ function completeRefresh() {
+ var results = self._applyQuery();
+ self.totalCount = results.totalCount;
+ $.changeArray.apply(null, [ self.itemsArray, "splice", 0, self.itemsArray.length ].concat(results.items));
+
+ if (options && options.completed && $.isFunction(options.completed)) {
+ options.completed();
+ }
+ completed();
+ };
+ },
+
+ _setFilter: function (filter) {
+ this._filter = (!filter || $.isFunction(filter)) ? filter : this._createFilterFunction(filter);
+ }
+ });
+
+ function RemoteDataSource (options) {
+ DataSource.apply(this, [ options ]);
+
+ this._urlMapper = options.urlMapper || function (path, queryParams) {
+ return path + queryParams;
+ };
+ this._path = options.path;
+ this._queryParams = options.queryParams;
+ this._resultsFilter = options.resultsFilter;
+ };
+
+ RemoteDataSource.prototype = $.extend({}, new DataSource(), {
+ _urlMapper: null,
+ _path: null,
+ _queryParams: null,
+ _resultsFilter: null,
+
+ _refresh: function (options, completed) {
+ var self = this,
+ queryString = this._urlMapper(this._path, this._queryParams, this._sortProperty,
+ this._sortDir, this._filter, this._skip, this._take, this._includeTotalCount);
+ $.ajax({
+ dataType: "jsonp",
+ url: queryString,
+ jsonp: "$callback",
+ success: function (data) {
+ var newItems = self._resultsFilter ? self._resultsFilter(data) : data;
+ // _resultsFilter has the option of setting this.totalCount.
+ $.changeArray.apply(null, [ self.itemsArray, "splice", 0, self.itemsArray.length ].concat(newItems));
+ completed();
+ }
+ });
+ },
+
+ _setFilter: function (filter) {
+ if (!filter) {
+ this._filter = null; // Passing null/undefined means clear filter.
+ } else if (Object.prototype.toString.call(filter) === "[object Array]") {
+ var self = this;
+ this._filter = $.map(filter, function (subfilter) {
+ return self._processFilter(subfilter);
+ });
+ } else {
+ this._filter = [ this._processFilter(filter) ];
+ }
+ }
+ });
+})(jQuery);
Please sign in to comment.
Something went wrong with that request. Please try again.