468 lines
18 KiB
JavaScript
Executable File
468 lines
18 KiB
JavaScript
Executable File
/*!
|
|
* Knockout ES5 plugin - https://github.com/SteveSanderson/knockout-es5
|
|
* Copyright (c) Steve Sanderson
|
|
* MIT license
|
|
*/
|
|
|
|
(function(global, undefined) {
|
|
'use strict';
|
|
|
|
var ko;
|
|
|
|
// Model tracking
|
|
// --------------
|
|
//
|
|
// This is the central feature of Knockout-ES5. We augment model objects by converting properties
|
|
// into ES5 getter/setter pairs that read/write an underlying Knockout observable. This means you can
|
|
// use plain JavaScript syntax to read/write the property while still getting the full benefits of
|
|
// Knockout's automatic dependency detection and notification triggering.
|
|
//
|
|
// For comparison, here's Knockout ES3-compatible syntax:
|
|
//
|
|
// var firstNameLength = myModel.user().firstName().length; // Read
|
|
// myModel.user().firstName('Bert'); // Write
|
|
//
|
|
// ... versus Knockout-ES5 syntax:
|
|
//
|
|
// var firstNameLength = myModel.user.firstName.length; // Read
|
|
// myModel.user.firstName = 'Bert'; // Write
|
|
|
|
// `ko.track(model)` converts each property on the given model object into a getter/setter pair that
|
|
// wraps a Knockout observable. Optionally specify an array of property names to wrap; otherwise we
|
|
// wrap all properties. If any of the properties are already observables, we replace them with
|
|
// ES5 getter/setter pairs that wrap your original observable instances. In the case of readonly
|
|
// ko.computed properties, we simply do not define a setter (so attempted writes will be ignored,
|
|
// which is how ES5 readonly properties normally behave).
|
|
//
|
|
// By design, this does *not* recursively walk child object properties, because making literally
|
|
// everything everywhere independently observable is usually unhelpful. When you do want to track
|
|
// child object properties independently, define your own class for those child objects and put
|
|
// a separate ko.track call into its constructor --- this gives you far more control.
|
|
/**
|
|
* @param {object} obj
|
|
* @param {object|array.<string>} propertyNamesOrSettings
|
|
* @param {boolean} propertyNamesOrSettings.deep Use deep track.
|
|
* @param {array.<string>} propertyNamesOrSettings.fields Array of property names to wrap.
|
|
* todo: @param {array.<string>} propertyNamesOrSettings.exclude Array of exclude property names to wrap.
|
|
* todo: @param {function(string, *):boolean} propertyNamesOrSettings.filter Function to filter property
|
|
* names to wrap. A function that takes ... params
|
|
* @return {object}
|
|
*/
|
|
function track(obj, propertyNamesOrSettings) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
throw new Error('When calling ko.track, you must pass an object as the first parameter.');
|
|
}
|
|
|
|
var propertyNames;
|
|
|
|
if ( isPlainObject(propertyNamesOrSettings) ) {
|
|
// defaults
|
|
propertyNamesOrSettings.deep = propertyNamesOrSettings.deep || false;
|
|
propertyNamesOrSettings.fields = propertyNamesOrSettings.fields || Object.getOwnPropertyNames(obj);
|
|
propertyNamesOrSettings.lazy = propertyNamesOrSettings.lazy || false;
|
|
|
|
wrap(obj, propertyNamesOrSettings.fields, propertyNamesOrSettings);
|
|
} else {
|
|
propertyNames = propertyNamesOrSettings || Object.getOwnPropertyNames(obj);
|
|
wrap(obj, propertyNames, {});
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
// fix for ie
|
|
var rFunctionName = /^function\s*([^\s(]+)/;
|
|
function getFunctionName( ctor ){
|
|
if (ctor.name) {
|
|
return ctor.name;
|
|
}
|
|
return (ctor.toString().trim().match( rFunctionName ) || [])[1];
|
|
}
|
|
|
|
function canTrack(obj) {
|
|
return obj && typeof obj === 'object' && getFunctionName(obj.constructor) === 'Object';
|
|
}
|
|
|
|
function createPropertyDescriptor(originalValue, prop, map) {
|
|
var isObservable = ko.isObservable(originalValue);
|
|
var isArray = !isObservable && Array.isArray(originalValue);
|
|
var observable = isObservable ? originalValue
|
|
: isArray ? ko.observableArray(originalValue)
|
|
: ko.observable(originalValue);
|
|
|
|
map[prop] = function () { return observable; };
|
|
|
|
// add check in case the object is already an observable array
|
|
if (isArray || (isObservable && 'push' in observable)) {
|
|
notifyWhenPresentOrFutureArrayValuesMutate(ko, observable);
|
|
}
|
|
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: observable,
|
|
set: ko.isWriteableObservable(observable) ? observable : undefined
|
|
};
|
|
}
|
|
|
|
function createLazyPropertyDescriptor(originalValue, prop, map) {
|
|
if (ko.isObservable(originalValue)) {
|
|
// no need to be lazy if we already have an observable
|
|
return createPropertyDescriptor(originalValue, prop, map);
|
|
}
|
|
|
|
var observable;
|
|
|
|
function getOrCreateObservable(value, writing) {
|
|
if (observable) {
|
|
return writing ? observable(value) : observable;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
observable = ko.observableArray(value);
|
|
notifyWhenPresentOrFutureArrayValuesMutate(ko, observable);
|
|
return observable;
|
|
}
|
|
|
|
return (observable = ko.observable(value));
|
|
}
|
|
|
|
map[prop] = function () { return getOrCreateObservable(originalValue); };
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function () { return getOrCreateObservable(originalValue)(); },
|
|
set: function (value) { getOrCreateObservable(value, true); }
|
|
};
|
|
}
|
|
|
|
function wrap(obj, props, options) {
|
|
if (!props.length) {
|
|
return;
|
|
}
|
|
|
|
var allObservablesForObject = getAllObservablesForObject(obj, true);
|
|
var descriptors = {};
|
|
|
|
props.forEach(function (prop) {
|
|
// Skip properties that are already tracked
|
|
if (prop in allObservablesForObject) {
|
|
return;
|
|
}
|
|
|
|
// Skip properties where descriptor can't be redefined
|
|
if (Object.getOwnPropertyDescriptor(obj, prop).configurable === false){
|
|
return;
|
|
}
|
|
|
|
var originalValue = obj[prop];
|
|
descriptors[prop] = (options.lazy ? createLazyPropertyDescriptor : createPropertyDescriptor)
|
|
(originalValue, prop, allObservablesForObject);
|
|
|
|
if (options.deep && canTrack(originalValue)) {
|
|
wrap(originalValue, Object.keys(originalValue), options);
|
|
}
|
|
});
|
|
|
|
Object.defineProperties(obj, descriptors);
|
|
}
|
|
|
|
function isPlainObject( obj ){
|
|
return !!obj && typeof obj === 'object' && obj.constructor === Object;
|
|
}
|
|
|
|
// Lazily created by `getAllObservablesForObject` below. Has to be created lazily because the
|
|
// WeakMap factory isn't available until the module has finished loading (may be async).
|
|
var objectToObservableMap;
|
|
|
|
// Gets or creates the hidden internal key-value collection of observables corresponding to
|
|
// properties on the model object.
|
|
function getAllObservablesForObject(obj, createIfNotDefined) {
|
|
if (!objectToObservableMap) {
|
|
objectToObservableMap = weakMapFactory();
|
|
}
|
|
|
|
var result = objectToObservableMap.get(obj);
|
|
if (!result && createIfNotDefined) {
|
|
result = {};
|
|
objectToObservableMap.set(obj, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Removes the internal references to observables mapped to the specified properties
|
|
// or the entire object reference if no properties are passed in. This allows the
|
|
// observables to be replaced and tracked again.
|
|
function untrack(obj, propertyNames) {
|
|
if (!objectToObservableMap) {
|
|
return;
|
|
}
|
|
|
|
if (arguments.length === 1) {
|
|
objectToObservableMap['delete'](obj);
|
|
} else {
|
|
var allObservablesForObject = getAllObservablesForObject(obj, false);
|
|
if (allObservablesForObject) {
|
|
propertyNames.forEach(function(propertyName) {
|
|
delete allObservablesForObject[propertyName];
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Computed properties
|
|
// -------------------
|
|
//
|
|
// The preceding code is already sufficient to upgrade ko.computed model properties to ES5
|
|
// getter/setter pairs (or in the case of readonly ko.computed properties, just a getter).
|
|
// These then behave like a regular property with a getter function, except they are smarter:
|
|
// your evaluator is only invoked when one of its dependencies changes. The result is cached
|
|
// and used for all evaluations until the next time a dependency changes).
|
|
//
|
|
// However, instead of forcing developers to declare a ko.computed property explicitly, it's
|
|
// nice to offer a utility function that declares a computed getter directly.
|
|
|
|
// Implements `ko.defineProperty`
|
|
function defineComputedProperty(obj, propertyName, evaluatorOrOptions) {
|
|
var ko = this,
|
|
computedOptions = { owner: obj, deferEvaluation: true };
|
|
|
|
if (typeof evaluatorOrOptions === 'function') {
|
|
computedOptions.read = evaluatorOrOptions;
|
|
} else {
|
|
if ('value' in evaluatorOrOptions) {
|
|
throw new Error('For ko.defineProperty, you must not specify a "value" for the property. ' +
|
|
'You must provide a "get" function.');
|
|
}
|
|
|
|
if (typeof evaluatorOrOptions.get !== 'function') {
|
|
throw new 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;
|
|
}
|
|
|
|
obj[propertyName] = ko.computed(computedOptions);
|
|
track.call(ko, obj, [propertyName]);
|
|
return obj;
|
|
}
|
|
|
|
// Array handling
|
|
// --------------
|
|
//
|
|
// Arrays are special, because unlike other property types, they have standard mutator functions
|
|
// (`push`/`pop`/`splice`/etc.) and it's desirable to trigger a change notification whenever one of
|
|
// those mutator functions is invoked.
|
|
//
|
|
// Traditionally, Knockout handles this by putting special versions of `push`/`pop`/etc. on observable
|
|
// arrays that mutate the underlying array and then trigger a notification. That approach doesn't
|
|
// work for Knockout-ES5 because properties now return the underlying arrays, so the mutator runs
|
|
// in the context of the underlying array, not any particular observable:
|
|
//
|
|
// // Operates on the underlying array value
|
|
// myModel.someCollection.push('New value');
|
|
//
|
|
// To solve this, Knockout-ES5 detects array values, and modifies them as follows:
|
|
// 1. Associates a hidden subscribable with each array instance that it encounters
|
|
// 2. Intercepts standard mutators (`push`/`pop`/etc.) and makes them trigger the subscribable
|
|
// Then, for model properties whose values are arrays, the property's underlying observable
|
|
// subscribes to the array subscribable, so it can trigger a change notification after mutation.
|
|
|
|
// Given an observable that underlies a model property, watch for any array value that might
|
|
// be assigned as the property value, and hook into its change events
|
|
function notifyWhenPresentOrFutureArrayValuesMutate(ko, observable) {
|
|
var watchingArraySubscription = null;
|
|
ko.computed(function () {
|
|
// Unsubscribe to any earlier array instance
|
|
if (watchingArraySubscription) {
|
|
watchingArraySubscription.dispose();
|
|
watchingArraySubscription = null;
|
|
}
|
|
|
|
// Subscribe to the new array instance
|
|
var newArrayInstance = observable();
|
|
if (newArrayInstance instanceof Array) {
|
|
watchingArraySubscription = startWatchingArrayInstance(ko, observable, newArrayInstance);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Listens for array mutations, and when they happen, cause the observable to fire notifications.
|
|
// This is used to make model properties of type array fire notifications when the array changes.
|
|
// Returns a subscribable that can later be disposed.
|
|
function startWatchingArrayInstance(ko, observable, arrayInstance) {
|
|
var subscribable = getSubscribableForArray(ko, arrayInstance);
|
|
return subscribable.subscribe(observable);
|
|
}
|
|
|
|
// Lazily created by `getSubscribableForArray` below. Has to be created lazily because the
|
|
// WeakMap factory isn't available until the module has finished loading (may be async).
|
|
var arraySubscribablesMap;
|
|
|
|
// Gets or creates a subscribable that fires after each array mutation
|
|
function getSubscribableForArray(ko, arrayInstance) {
|
|
if (!arraySubscribablesMap) {
|
|
arraySubscribablesMap = weakMapFactory();
|
|
}
|
|
|
|
var subscribable = arraySubscribablesMap.get(arrayInstance);
|
|
if (!subscribable) {
|
|
subscribable = new ko.subscribable();
|
|
arraySubscribablesMap.set(arrayInstance, subscribable);
|
|
|
|
var notificationPauseSignal = {};
|
|
wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal);
|
|
addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal);
|
|
}
|
|
|
|
return subscribable;
|
|
}
|
|
|
|
// After each array mutation, fires a notification on the given subscribable
|
|
function wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal) {
|
|
['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'].forEach(function(fnName) {
|
|
var origMutator = arrayInstance[fnName];
|
|
arrayInstance[fnName] = function() {
|
|
var result = origMutator.apply(this, arguments);
|
|
if (notificationPauseSignal.pause !== true) {
|
|
subscribable.notifySubscribers(this);
|
|
}
|
|
return result;
|
|
};
|
|
});
|
|
}
|
|
|
|
// Adds Knockout's additional array mutation functions to the array
|
|
function addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal) {
|
|
['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'].forEach(function(fnName) {
|
|
// Make it a non-enumerable property for consistency with standard Array functions
|
|
Object.defineProperty(arrayInstance, fnName, {
|
|
enumerable: false,
|
|
value: function() {
|
|
var result;
|
|
|
|
// These additional array mutators are built using the underlying push/pop/etc.
|
|
// mutators, which are wrapped to trigger notifications. But we don't want to
|
|
// trigger multiple notifications, so pause the push/pop/etc. wrappers and
|
|
// delivery only one notification at the end of the process.
|
|
notificationPauseSignal.pause = true;
|
|
try {
|
|
// Creates a temporary observableArray that can perform the operation.
|
|
result = ko.observableArray.fn[fnName].apply(ko.observableArray(arrayInstance), arguments);
|
|
}
|
|
finally {
|
|
notificationPauseSignal.pause = false;
|
|
}
|
|
subscribable.notifySubscribers(arrayInstance);
|
|
return result;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Static utility functions
|
|
// ------------------------
|
|
//
|
|
// Since Knockout-ES5 sets up properties that return values, not observables, you can't
|
|
// trivially subscribe to the underlying observables (e.g., `someProperty.subscribe(...)`),
|
|
// or tell them that object values have mutated, etc. To handle this, we set up some
|
|
// extra utility functions that can return or work with the underlying observables.
|
|
|
|
// Returns the underlying observable associated with a model property (or `null` if the
|
|
// model or property doesn't exist, or isn't associated with an observable). This means
|
|
// you can subscribe to the property, e.g.:
|
|
//
|
|
// ko.getObservable(model, 'propertyName')
|
|
// .subscribe(function(newValue) { ... });
|
|
function getObservable(obj, propertyName) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
var allObservablesForObject = getAllObservablesForObject(obj, false);
|
|
if (allObservablesForObject && propertyName in allObservablesForObject) {
|
|
return allObservablesForObject[propertyName]();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Returns a boolean indicating whether the property on the object has an underlying
|
|
// observables. This does the check in a way not to create an observable if the
|
|
// object was created with lazily created observables
|
|
function isTracked(obj, propertyName) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
var allObservablesForObject = getAllObservablesForObject(obj, false);
|
|
return !!allObservablesForObject && propertyName in allObservablesForObject;
|
|
}
|
|
|
|
// Causes a property's associated observable to fire a change notification. Useful when
|
|
// the property value is a complex object and you've modified a child property.
|
|
function valueHasMutated(obj, propertyName) {
|
|
var observable = getObservable(obj, propertyName);
|
|
|
|
if (observable) {
|
|
observable.valueHasMutated();
|
|
}
|
|
}
|
|
|
|
// Module initialisation
|
|
// ---------------------
|
|
//
|
|
// When this script is first evaluated, it works out what kind of module loading scenario
|
|
// it is in (Node.js or a browser `<script>` tag), stashes a reference to its dependencies
|
|
// (currently that's just the WeakMap shim), and then finally attaches itself to whichever
|
|
// instance of Knockout.js it can find.
|
|
|
|
// A function that returns a new ES6-compatible WeakMap instance (using ES5 shim if needed).
|
|
// Instantiated by prepareExports, accounting for which module loader is being used.
|
|
var weakMapFactory;
|
|
|
|
// Extends a Knockout instance with Knockout-ES5 functionality
|
|
function attachToKo(ko) {
|
|
ko.track = track;
|
|
ko.untrack = untrack;
|
|
ko.getObservable = getObservable;
|
|
ko.valueHasMutated = valueHasMutated;
|
|
ko.defineProperty = defineComputedProperty;
|
|
|
|
// todo: test it, maybe added it to ko. directly
|
|
ko.es5 = {
|
|
getAllObservablesForObject: getAllObservablesForObject,
|
|
notifyWhenPresentOrFutureArrayValuesMutate: notifyWhenPresentOrFutureArrayValuesMutate,
|
|
isTracked: isTracked
|
|
};
|
|
}
|
|
|
|
// Determines which module loading scenario we're in, grabs dependencies, and attaches to KO
|
|
function prepareExports() {
|
|
if (typeof exports === 'object' && typeof module === 'object') {
|
|
// Node.js case - load KO and WeakMap modules synchronously
|
|
ko = require('knockout');
|
|
var WM = require('../lib/weakmap');
|
|
attachToKo(ko);
|
|
weakMapFactory = function() { return new WM(); };
|
|
module.exports = ko;
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define(['knockout'], function(koModule) {
|
|
ko = koModule;
|
|
attachToKo(koModule);
|
|
weakMapFactory = function() { return new global.WeakMap(); };
|
|
return koModule;
|
|
});
|
|
} else if ('ko' in global) {
|
|
// Non-module case - attach to the global instance, and assume a global WeakMap constructor
|
|
ko = global.ko;
|
|
attachToKo(global.ko);
|
|
weakMapFactory = function() { return new global.WeakMap(); };
|
|
}
|
|
}
|
|
|
|
prepareExports();
|
|
|
|
})(this); |