grunt-durandal
Version:
Grunt Durandal Builder - Build durandal project using a custom require config and a custom almond
333 lines (271 loc) • 11.4 kB
JavaScript
/**
* Durandal 2.0.0 Copyright (c) 2012 Blue Spire Consulting, Inc. All Rights Reserved.
* Available via the MIT license.
* see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details.
*/
/**
* Enables automatic observability of plain javascript object for ES5 compatible browsers. Also, converts promise properties into observables that are updated when the promise resolves.
* @module observable
* @requires system
* @requires binder
* @requires knockout
*/
define(['durandal/system', 'durandal/binder', 'knockout'], function(system, binder, ko) {
var observableModule,
toString = Object.prototype.toString,
nonObservableTypes = ['[object Function]', '[object String]', '[object Boolean]', '[object Number]', '[object Date]', '[object RegExp]'],
observableArrayMethods = ['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'],
arrayMethods = ['pop', 'reverse', 'sort', 'shift', 'splice'],
additiveArrayFunctions = ['push', 'unshift'],
arrayProto = Array.prototype,
observableArrayFunctions = ko.observableArray.fn,
logConversion = false;
/**
* You can call observable(obj, propertyName) to get the observable function for the specified property on the object.
* @class ObservableModule
*/
function shouldIgnorePropertyName(propertyName){
var first = propertyName[0];
return first === '_' || first === '$';
}
function canConvertType(value) {
if (!value || system.isElement(value) || value.ko === ko || value.jquery) {
return false;
}
var type = toString.call(value);
return nonObservableTypes.indexOf(type) == -1 && !(value === true || value === false);
}
function makeObservableArray(original, observable) {
var lookup = original.__observable__, notify = true;
if(lookup && lookup.__full__){
return;
}
lookup = lookup || (original.__observable__ = {});
lookup.__full__ = true;
observableArrayMethods.forEach(function(methodName) {
original[methodName] = function() {
notify = false;
var methodCallResult = observableArrayFunctions[methodName].apply(observable, arguments);
notify = true;
return methodCallResult;
};
});
arrayMethods.forEach(function(methodName) {
original[methodName] = function() {
if(notify){
observable.valueWillMutate();
}
var methodCallResult = arrayProto[methodName].apply(original, arguments);
if(notify){
observable.valueHasMutated();
}
return methodCallResult;
};
});
additiveArrayFunctions.forEach(function(methodName){
original[methodName] = function() {
for (var i = 0, len = arguments.length; i < len; i++) {
convertObject(arguments[i]);
}
if(notify){
observable.valueWillMutate();
}
var methodCallResult = arrayProto[methodName].apply(original, arguments);
if(notify){
observable.valueHasMutated();
}
return methodCallResult;
};
});
original['splice'] = function() {
for (var i = 2, len = arguments.length; i < len; i++) {
convertObject(arguments[i]);
}
if(notify){
observable.valueWillMutate();
}
var methodCallResult = arrayProto['splice'].apply(original, arguments);
if(notify){
observable.valueHasMutated();
}
return methodCallResult;
};
for (var i = 0, len = original.length; i < len; i++) {
convertObject(original[i]);
}
}
/**
* Converts an entire object into an observable object by re-writing its attributes using ES5 getters and setters. Attributes beginning with '_' or '$' are ignored.
* @method convertObject
* @param {object} obj The target object to convert.
*/
function convertObject(obj){
var lookup, value;
if(!canConvertType(obj)){
return;
}
lookup = obj.__observable__;
if(lookup && lookup.__full__){
return;
}
lookup = lookup || (obj.__observable__ = {});
lookup.__full__ = true;
if (system.isArray(obj)) {
var observable = ko.observableArray(obj);
makeObservableArray(obj, observable);
} else {
for (var propertyName in obj) {
if(shouldIgnorePropertyName(propertyName)){
continue;
}
if(!lookup[propertyName]){
value = obj[propertyName];
if(!system.isFunction(value)){
convertProperty(obj, propertyName, value);
}
}
}
}
if(logConversion) {
system.log('Converted', obj);
}
}
function innerSetter(observable, newValue, isArray) {
var val;
observable(newValue);
val = observable.peek();
//if this was originally an observableArray, then always check to see if we need to add/replace the array methods (if newValue was an entirely new array)
if (isArray) {
if (!val.destroyAll) {
//don't allow null, force to an empty array
if (!val) {
val = [];
observable(val);
}
makeObservableArray(val, observable);
}
} else {
convertObject(val);
}
}
/**
* Converts a normal property into an observable property using ES5 getters and setters.
* @method convertProperty
* @param {object} obj The target object on which the property to convert lives.
* @param {string} propertyName The name of the property to convert.
* @param {object} [original] The original value of the property. If not specified, it will be retrieved from the object.
* @return {KnockoutObservable} The underlying observable.
*/
function convertProperty(obj, propertyName, original){
var observable,
isArray,
lookup = obj.__observable__ || (obj.__observable__ = {});
if(original === undefined){
original = obj[propertyName];
}
if (system.isArray(original)) {
observable = ko.observableArray(original);
makeObservableArray(original, observable);
isArray = true;
} else if (typeof original == "function") {
if(ko.isObservable(original)){
observable = original;
}else{
return null;
}
} else if(system.isPromise(original)) {
observable = ko.observable();
original.then(function (result) {
if(system.isArray(result)) {
var oa = ko.observableArray(result);
makeObservableArray(result, oa);
result = oa;
}
observable(result);
});
} else {
observable = ko.observable(original);
convertObject(original);
}
Object.defineProperty(obj, propertyName, {
configurable: true,
enumerable: true,
get: observable,
set: ko.isWriteableObservable(observable) ? (function (newValue) {
if (newValue && system.isPromise(newValue)) {
newValue.then(function (result) {
innerSetter(observable, result, system.isArray(result));
});
} else {
innerSetter(observable, newValue, isArray);
}
}) : undefined
});
lookup[propertyName] = observable;
return observable;
}
/**
* Defines a computed property using ES5 getters and setters.
* @method defineProperty
* @param {object} obj The target object on which to create the property.
* @param {string} propertyName The name of the property to define.
* @param {function|object} evaluatorOrOptions The Knockout computed function or computed options object.
* @return {KnockoutComputed} The underlying computed observable.
*/
function defineProperty(obj, propertyName, evaluatorOrOptions) {
var ko = this,
computedOptions = { owner: obj, deferEvaluation: true },
computed;
if (typeof evaluatorOrOptions === 'function') {
computedOptions.read = evaluatorOrOptions;
} else {
if ('value' in evaluatorOrOptions) {
system.error('For ko.defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');
}
if (typeof evaluatorOrOptions.get !== 'function') {
system.error('For ko.defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');
}
computedOptions.read = evaluatorOrOptions.get;
computedOptions.write = evaluatorOrOptions.set;
}
computed = ko.computed(computedOptions);
obj[propertyName] = computed;
return convertProperty(obj, propertyName, computed);
}
observableModule = function(obj, propertyName){
var lookup, observable, value;
if (!obj) {
return null;
}
lookup = obj.__observable__;
if(lookup){
observable = lookup[propertyName];
if(observable){
return observable;
}
}
value = obj[propertyName];
if(ko.isObservable(value)){
return value;
}
return convertProperty(obj, propertyName, value);
};
observableModule.defineProperty = defineProperty;
observableModule.convertProperty = convertProperty;
observableModule.convertObject = convertObject;
/**
* Installs the plugin into the view model binder's `beforeBind` hook so that objects are automatically converted before being bound.
* @method install
*/
observableModule.install = function(options) {
var original = binder.binding;
binder.binding = function(obj, view, instruction) {
if(instruction.applyBindings && !instruction.skipConversion){
convertObject(obj);
}
original(obj, view);
};
logConversion = options.logConversion;
};
return observableModule;
});