Skip to content

Instantly share code, notes, and snippets.

@RainerAtSpirit
Created April 8, 2013 11:43
Show Gist options
  • Save RainerAtSpirit/5336223 to your computer and use it in GitHub Desktop.
Save RainerAtSpirit/5336223 to your computer and use it in GitHub Desktop.
PromiseDemo: Using SPServiceAlpha as data provider for a Durandal app
<section>
<h2 class="page-title" data-bind="text: title"></h2>
<div class="row-fluid">
<div class="span6">
<label>Select a list</label>
<select data-bind="options: lists, optionsText: 'name', value: activeList"></select>
</div>
<div class="span6 well">
<h3 data-bind="text: activeList().listName"></h3>
<div data-bind="text: activeList().description"></div>
</div>
</div>
<!--ko compose: activeList--><!--/ko-->
</section>
/*global define, ko, L_Menu_BaseUrl*/
define(['./list', 'durandal/viewModel', 'services/logger', 'services/spdata'],
function (List, viewModel, logger, spdata) {
var webUrl = ko.observable();
var lists = ko.observableArray([]);
return {
title: 'Promises demo',
webUrl: webUrl,
lists: lists,
activeList: viewModel.activator().forItems(lists),
activate: activate
};
//#region Internal Methods
function activate() {
var self = this;
var webUrl = '';
// Checking for edge case when running in top level site collection
webUrl = (typeof L_Menu_BaseUrl !== 'undefined' && L_Menu_BaseUrl !== '') ? L_Menu_BaseUrl : '../';
self.webUrl(webUrl);
logger.log('Activatíng List View', null, 'list', true);
return $.when(spdata.getListCollection({ webUrl: webUrl })).then(function (store) {
var listDropDown = [];
$.each(store, function (key, obj) {
if (!obj.Hidden) {
listDropDown.push(new List({
name: obj.Title,
webUrl: self.webUrl(),
listName: obj.Title,
// optional fields for detail information block
description: obj.Description
})
);
}
});
//
self.lists(listDropDown);
});
}
//#endregion
});
<div>
<div class="row-fluid">
<div class="span6">
<h4>paging</h4>
<div>
<select data-bind="value: _pageSize, options: [10, 20, 50]"></select>
Page: <span data-bind=" text: page"></span>
</div>
Page Size:
<div class="btn-group">
<button class="btn" data-bind="css: { disabled: !hasPrevious() }, click: previous ">&larr; previous</button>
<button class="btn" data-bind="css: { disabled: !hasNext() }, click: next ">next &rarr;</button>
</div>
</div>
<div class="span6">
<h4>sorting</h4>
<div data-bind="foreach: _sort()">
<!-- Todo: convert to Durandal compose -->
<select data-bind="options: $parent.fields, optionsText: 'DisplayName', optionsValue: 'StaticName', value: field"></select>
<!--<select data-bind="options: ['ID', 'Title', 'Modified'], value: field"></select>
-->
<select data-bind="options: ['asc', 'desc'], value: dir"></select>
</div>
</div>
</div>
<h4>Result</h4>
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<td>ID</td>
<td>Title</td>
<td>Modified</td>
</tr>
</thead>
<tbody data-bind="foreach: listItems">
<tr>
<td data-bind="text: ID"></td>
<td data-bind="text: Title"></td>
<td data-bind="text: Modified"></td>
</tr>
</tbody>
</table>
</div>
/*global define, ko */
define(['services/spdata'],
function (spdata) {
var Sort = function (options) {
var self = this;
this.field = ko.observable(options.field);
this.dir = ko.observable(options.dir);
};
var ctor = function (options) {
var self = this;
// Preserve constructor options
self.options = options || {};
this.listInfo = {};
this.pagingHistory = [''];
this.fields = [];
this.viewFields = [];
// ko observables
this.ListItemCollectionPositionNext = ko.observable('');
this.listItems = ko.observableArray([]);
this.fetching = ko.observable(true);
this.page = ko.observable(1);
this.ItemCount = ko.observable();
this._pageSize = ko.observable();
this._sort = ko.observableArray([]);
// ko computed
this.hasPrevious = ko.computed(function () {
return (self.page() > 1);
});
this.hasNext = ko.computed(function () {
return (self.ListItemCollectionPositionNext() !== '');
});
this.init(options);
// refresh and fetch new data on observable change
$.each(['_pageSize', '_sort'], function (idx, obj) {
self[obj].subscribe(function (val) {
self.refresh();
});
});
};
ctor.prototype.init = function (options) {
//Todo: Check mandatory parameter
this.name = options.name;
this.webUrl = options.webUrl;
this.listName = options.listName;
this.description = options.description || 'Default description';
this._pageSize(options.pageSize || 10);
this.viewFields = options.viewFields || ['Title', 'Editor'];
// Do NOT call sort(option.sort) here as it would trigger a refresh
options.sort = options.sort || { field: 'ID', dir: 'asc' };
};
ctor.prototype.refresh = function () {
this.pagingHistory = [''];
this.page(1);
this.fetch();
};
ctor.prototype.key = function () {
var path = this.webUrl.replace(/^\/+|\/+$/g, '');
return '/' + path.toLowerCase() + '/' + this.listName.toLowerCase();
};
ctor.prototype.fetch = function () {
var self = this;
spdata.getListItems(spdata.getListItemsOptions(this), this.listInfo.SPXmlToJsonMap).then(function (json) {
//ko observables
self.listItems(json.data);
self.ListItemCollectionPositionNext(json.ListItemCollectionPositionNext);
self.ItemCount = (json.ItemCount);
self.pagingHistory.push(json.ListItemCollectionPositionNext);
});
};
ctor.prototype.next = function () {
if (this.ListItemCollectionPositionNext() === '') {
return false;
}
this.page(this.page() + 1);
this.fetch();
};
ctor.prototype.previous = function () {
var spliceValue = 0;
if (this.pagingHistory.length < 3) {
return false;
}
this.page(this.page() - 1);
// GetListItems method calculates ListItemPositionNext on each request, so we remove entries from pagingHistory
spliceValue = (this.page() - this.pagingHistory.length);
this.pagingHistory.splice(spliceValue);
this.fetch();
};
ctor.prototype.sort = function (options) {
var self = this;
var sortArray = [];
// ToDo: Check normalisation
options = $.isArray(options) ? options : [options];
$.each(options, function (idx, obj) {
var sort = new Sort(obj);
// Setting up change event
$.each(['field', 'dir'], function (idx, obj) {
sort[obj].subscribe(function (val) {
self.refresh();
});
});
sortArray.push(sort);
});
this._sort(sortArray);
};
// Durandal viewmodel function
ctor.prototype.activate = function () {
var self = this;
var getModelOptions = {
listName: this.listName,
webUrl: this.webUrl,
key: this.key()
};
// Step 1: Create a Json list model based on SPServices 'GetList' method
// spdata.getModel() returns either a promise or a cached result -> use $.when() to resolve
// see http://lostechies.com/derickbailey/2012/03/27/providing-synchronous-asynchronous-flexibility-with-jquery-when/
$.when(spdata.getModel(getModelOptions)).then(function (data) {
// Todo: Move to viemmodel init method and call self.init(data) instead
//store produced json
self.listInfo = data;
$.each(data.fields, function (prop, obj) {
if (!obj.Hidden) {
if (typeof obj.Sortable === 'undefined') {
self.fields.push(obj);
return;
}
if (obj.Sortable) {
self.fields.push(obj);
return;
}
}
});
// apply inital sort options
self.sort(self.options.sort);
// Step 2: Fetch the actual data
//self.fetch();
});
// Expose the listModel globally
sp3.dataSources[this.key()] = this;
};
ctor.prototype.deactivate = function () {
delete sp3.dataSources[this.key()];
};
return ctor;
});
/*global define, sp3 */
define(['./logger'],
function (logger) {
var getListCollection = function (options) {
if (sp3.webs[options.webUrl]) {
logger.log('cached ListCollection', sp3.webs[options.webUrl], 'spdata.getListCollection()', true);
return sp3.webs[options.webUrl];
}
var getListCollectionPromise = $().SPServices({
operation: "GetListCollection",
webURL: options.webUrl
});
return $.when(getListCollectionPromise)
.then(function (data) {
logger.log('resolve getListCollectionPromise', mapGetListCollection2Json(data), 'spdata.getListCollection', true);
// Store for caching purposes
sp3.webs[options.webUrl] = mapGetListCollection2Json(data);
return sp3.webs[options.webUrl];
})
.fail(function (data) {
logger.logError('Error', data);
});
};
var getModel = function (options) {
// Return cached result if available
if (sp3.models[options.key]) {
logger.log('cached model', sp3.models[options.key], 'spdata.getModel()', true);
return sp3.models[options.key];
}
var getListPromise = $().SPServices({
operation: "GetList",
listName: options.listName,
webURL: options.webUrl
});
return $.when(getListPromise)
.then(function (data) {
logger.log('resolve getListPromise', mapGetListResult2Json(data), 'spdata.getModel()', true);
return mapGetListResult2Json(data);
})
.fail(function (data) {
logger.logError('Error', data);
});
};
var getListItems = function (options, SPXmlToJsonMap) {
var getListItemsPromise = $().SPServices(options);
SPXmlToJsonMap = SPXmlToJsonMap || {};
// Step 2: GetListItems using SPXmlToJsonMap if available
return $.when(getListItemsPromise)
.then(function (data) {
var $data = $(data);
var json = {
ItemCount: parseInt($data.SPFilterNode("rs:data").attr('ItemCount'), 10) || 0,
ListItemCollectionPositionNext: $data.SPFilterNode("rs:data").attr('ListItemCollectionPositionNext') || '',
data: $data.SPFilterNode("z:row").SPXmlToJson({
mapping: SPXmlToJsonMap,
includeAllAttrs: true,
removeOws: false
})
};
logger.log('resolve getListItemsPromise', json, 'spdata.getListItems()', true);
return json;
})
.fail(function (data) {
logger.logError('Error', data);
});
};
var getListItemsOptions = function (options) {
var CAMLQueryOptions = createQueryOptions({
page: options.page(),
pagingHistory: options.pagingHistory
});
var CAMLQuery = createCAMLQuery({
sortExpression : options._sort()
});
var CAMLViewFields = createCAMLViewFields({
viewFields: options.viewFields
});
return {
operation: "GetListItems",
async: false,
webURL: options.webUrl,
listName: options.listName,
CAMLQuery: CAMLQuery,
CAMLRowLimit: options._pageSize(),
CAMLViewFields: CAMLViewFields,
CAMLQueryOptions: CAMLQueryOptions
};
};
return {
getListCollection: getListCollection,
getListItems: getListItems,
getListItemsOptions: getListItemsOptions,
getModel: getModel
};
//#region Internal Methods
function createCAMLViewFields(options) {
var result = [];
var viewFields = options.viewFields || ['Title'];
result.push('<ViewFields>');
$.each(viewFields, function (idx, field) {
result.push('<FieldRef Name="');
result.push(field);
result.push('" />');
});
result.push('</ViewFields>');
return result.join('');
}
function createCAMLQuery(options) {
var query = [];
query.push('<Query>');
// sorting
if (options.sortExpression.length > 0) {
query.push('<OrderBy>');
// create Caml sort expression
$.each(options.sortExpression, function (index, sortObj) {
sortDir = (sortObj.dir() === 'asc');
query.push('<FieldRef Name="' + sortObj.field() + '" Ascending="' + sortDir + '"/>');
});
query.push('</OrderBy>');
}
// Todo filtering
query.push('</Query>');
return query.join('');
}
function createQueryOptions(options) {
var PID = '';
var queryOptions = [];
// Todo: expose other QueryOptions
queryOptions.push('<QueryOptions>');
queryOptions.push('<DateInUtc>False</DateInUtc>');
queryOptions.push('<ExpandUserField>True</ExpandUserField>');
if (options.page > 1) {
PID = options.pagingHistory[options.page - 1].replace(/&/g, '&amp;');
}
queryOptions.push('<Paging ListItemCollectionPositionNext="' + PID + '"/>');
queryOptions.push('</QueryOptions>');
return queryOptions.join('');
}
function mapGetListCollection2Json(data) {
var store = {};
$(data).SPFilterNode('List').each(function (idx, obj) {
var propStore = {};
var storeID;
// See http://stackoverflow.com/questions/828311/how-to-iterate-through-all-attributes-in-an-html-element
for (var i = 0; i < obj.attributes.length; i++) {
var attrib = obj.attributes[i];
if (attrib.specified) {
if (attrib.name === 'Title') {
storeID = attrib.value.toLowerCase();
}
// mapping of text values that are stored as mixed case TRUE false
if (attrib.value.toLowerCase() === 'false') {
propStore[attrib.name] = false;
}
else if (attrib.value.toLowerCase() === 'true') {
propStore[attrib.name] = true;
}
else {
propStore[attrib.name] = attrib.value;
}
}
}
storeID = (propStore.WebFullUrl + '/' + storeID).toLowerCase();
store[storeID] = propStore;
});
return store;
}
function mapGetListResult2Json(data, Xml2JsonMap) {
var listInfo = {};
var SPXmlToJsonMap = listInfo.SPXmlToJsonMap = {};
var storeKey;
// Xml2JsonMap configuration
// Filternode: XML element used as filter
// PropertyID: XML attribute used as property. false store properties on root object
Xml2JsonMap = Xml2JsonMap || {
fields: { FilterNode: 'Field', PropertyID: 'StaticName' },
info: { FilterNode: 'List', PropertyID: false }
};
$.each(Xml2JsonMap, function (key, val) {
var store = listInfo[key] = {};
$(data).SPFilterNode(val.FilterNode).each(function (idx, obj) {
var propStore = {};
var storeID;
// See http://stackoverflow.com/questions/828311/how-to-iterate-through-all-attributes-in-an-html-element
for (var i = 0; i < obj.attributes.length; i++) {
var attrib = obj.attributes[i];
if (attrib.specified) {
if (attrib.name === val.PropertyID) {
storeID = attrib.value;
store[storeID] = {};
}
// mapping of text values that are stored as mixed case TRUE false
if (attrib.value.toLowerCase() === 'false') {
propStore[attrib.name] = false;
}
else if (attrib.value.toLowerCase() === 'true') {
propStore[attrib.name] = true;
}
else {
propStore[attrib.name] = attrib.value;
}
}
}
if (val.PropertyID && storeID) {
store[storeID] = propStore;
}
else if (!val.PropertyID) {
listInfo[key] = propStore;
}
});
});
// storeKey match logic of ctor.prototype.key = function () {...
storeKey = (listInfo.info.WebFullUrl + '/' + listInfo.info.Title).toLowerCase();
// Store cached result in global sp3.models using storeKey
sp3.models[storeKey] = listInfo;
// Add SPXmlToJson mapping
$.each(listInfo.fields, function (name, properties) {
SPXmlToJsonMap['ows_' + name] = {
mappedName: name,
objectType: properties.Type
};
});
return listInfo;
}
//#endregion
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment