UNPKG

knockout-viewmodel

Version:

Fork from The knockout viewmodel plugin is the fastest, smallest, cleanest, most flexible way to create a knockout viewmodel. With typescript and Module support.

436 lines (399 loc) 22.1 kB
/*ko.viewmodel.js - version 2.0.4 * Copyright 2013, Dave Herren http://coderenaissance.github.com/knockout.viewmodel/ * License: MIT (http://www.opensource.org/licenses/mit-license.php)*/ /*jshint eqnull:true, boss:true, loopfunc:true, evil:true, laxbreak:true, undef:true, unused:true, browser:true, immed:true, devel:true, sub: true, maxerr:50 */ /*global ko:false */ (function (factory) { if(typeof module === "object" && typeof module.exports === "object") { module.exports = factory(require("knockout")); } else if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['knockout'], function (ko) { // Also create a global in case some scripts // that are loaded still are looking for // a global even when an AMD loader is in use. var obj = factory(ko) return (root.ko ? root.ko.viewmodel = obj : obj); }); } else { factory(ko); } }(function (ko) { //Module declarations. For increased compression with simple settings on the closure compiler, //the ko functions are stored in variables. These variable names will be shortened by the compiler, //whereas references to ko would not be. There is also a performance savings from this. var unwrap = ko.utils.unwrapObservable, isObservable = ko.isObservable, makeObservable = ko.observable, makeObservableArray = ko.observableArray, rootContext = { name: "{root}", parent: "{root}", full: "{root}" }, fnLog, makeChildArraysObservable, badResult = function fnBadResult() { }; //Gets settings for the specified path function getPathSettings(settings, context) { //Settings for more specific paths are chosen over less specific ones. var pathSettings = settings ? settings[context.full] || settings[context.parent] || settings[context.name] || {} : {}; if (fnLog) fnLog(context, pathSettings, settings);//log what mapping will be used return pathSettings; } //Converts options into a dictionary of path settings //This allows for path settings to be looked up efficiently function getPathSettingsDictionary(options) { var result = {}, shared = options ? options.shared || {} : {}, settings, fn, index, key, length, settingType, childName, child; for (settingType in options) { settings = options[settingType] || {}; //Settings can either be dictionaries(associative arrays) or arrays //ignore shared option... contains functions that can be assigned by name if (settingType === "shared") continue; else if (settings instanceof Array) {//process array list for append and exclude for (index = 0, length = settings.length; index < length; index++) { key = settings[index]; result[key] = result[key] || {}; result[key][settingType] = true; result[key].settingType = result[key].settingType ? "multiple" : settingType; } } else if(settings.constructor === Object){//process associative array for extend and map for (key in settings) { result[key] = result[key] || {}; fn = settings[key]; fn = settingType !== "arrayChildId" && fn && fn.constructor === String && shared[fn] ? shared[fn] : fn; if (fn && fn.constructor === Object) {//associative array for map/unmap passed in instead of map function for (childName in fn) { //if children of fn are strings then replace with shared function if available if ((child = fn[childName]) && (child.constructor == String) && shared[child]) { fn[childName] = shared[child]; } } } result[key][settingType] = fn; result[key].settingType = result[key].settingType ? "multiple" : settingType; } } } return result; } function isNullOrUndefined(obj) {//checks if obj is null or undefined return obj === null || obj === undefined; } //while dates aren't part of the JSON spec it doesn't hurt to support them as it's not unreasonable to think they might be added to the model manually. //undefined is also not part of the spec, but it's currently be supported to be more in line with ko.mapping and probably doesn't hurt. function isPrimativeOrDate(obj) { return obj === null || obj === undefined || obj.constructor === String || obj.constructor === Number || obj.constructor === Boolean || obj instanceof Date; } function recrusiveFrom(modelObj, settings, context, pathSettings) { var temp, result, p, length, idName, newContext, customPathSettings, extend, optionProcessed, pathSettings = pathSettings || getPathSettings(settings, context), childPathSettings, childObj; if (customPathSettings = pathSettings.custom) { optionProcessed = true; //custom can either be specified as a single map function or as an //object with map and unmap properties if (typeof customPathSettings === "function") { result = customPathSettings(modelObj); } else { result = customPathSettings.map(modelObj); if (!isNullOrUndefined(result)) {//extend object with mapping info where possible result.___$mapCustom = customPathSettings.map;//preserve map function for updateFromModel calls if (customPathSettings.unmap) {//perserve unmap function for toModel calls result.___$unmapCustom = customPathSettings.unmap; } } } } else if (pathSettings.append) {//append property optionProcessed = true; result = modelObj;//append } else if (pathSettings.exclude) { optionProcessed = true; return badResult; } else if (isPrimativeOrDate(modelObj)) { //primative and date children of arrays aren't mapped... all others are result = context.parentIsArray ? modelObj : makeObservable(modelObj); } else if (modelObj instanceof Array) { result = []; for (p = 0, length = modelObj.length; p < length; p++) { result[p] = recrusiveFrom(modelObj[p], settings, { name: "[i]", parent: context.name + "[i]", full: context.full + "[i]", parentIsArray: true }); } //only makeObservableArray extend with mapping functions if it's not a nested array or mapping compatabitlity is off if (!context.parentIsArray || makeChildArraysObservable) { newContext = { name: "[i]", parent: context.name + "[i]", full: context.full + "[i]", parentIsArray: true }; result = makeObservableArray(result); //if available add id name to object so it can be accessed later when updating children if (idName = pathSettings.arrayChildId) { result.___$childIdName = idName; } //wrap array methods for adding and removing items in functions that //close over settings and context allowing the objects and their children to be correctly mapped. result.pushFromModel = function (item) { item = recrusiveFrom(item, settings, newContext); result.push(item); }; result.unshiftFromModel = function (item) { item = recrusiveFrom(item, settings, newContext); result.unshift(item); }; result.popToModel = function (item) { item = result.pop(); return recrusiveTo(item, newContext); }; result.shiftToModel = function (item) { item = result.shift(); return recrusiveTo(item, newContext); }; } } else if (modelObj.constructor === Object) { result = {}; for (p in modelObj) { newContext = { name: p, parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, full: context.full + "." + p }; childObj = modelObj[p]; childPathSettings = isPrimativeOrDate(childObj) ? getPathSettings(settings, newContext) : undefined; if (childPathSettings && childPathSettings.custom) {//primativish value w/ custom maping //since primative children cannot store their own custom functions, handle processing here and store them in the parent result.___$customChildren = result.___$customChildren || {}; result.___$customChildren[p] = childPathSettings.custom; if (typeof childPathSettings.custom === "function") { result[p] = childPathSettings.custom(modelObj[p]); } else { result[p] = childPathSettings.custom.map(modelObj[p]); } } else { temp = recrusiveFrom(childObj, settings, newContext, childPathSettings);//call recursive from on each child property if (temp !== badResult) {//properties that couldn't be mapped return badResult result[p] = temp; } } } } if (!optionProcessed && (extend = pathSettings.extend)) { if (typeof extend === "function") {//single map function specified //Extend can either modify the mapped value or replace it //Falsy values assumed to be undefined result = extend(result) || result; } else if (extend.constructor === Object) {//map and/or unmap were specified as part of object if (typeof extend.map === "function") { result = extend.map(result) || result;//use map to get result } if (typeof extend.unmap === "function") { result.___$unmapExtend = extend.unmap;//store unmap for use by toModel } } } return result; } function recrusiveTo(viewModelObj, context) { var result, p, length, temp, unwrapped = unwrap(viewModelObj), child, recursiveResult, wasWrapped = (viewModelObj !== unwrapped);//this works because unwrap observable calls isObservable and returns the object unchanged if not observable if (fnLog) { fnLog(context);//log object being unmapped } if (!wasWrapped && viewModelObj && viewModelObj.constructor === Function) {//Exclude functions return badResult; } else if (viewModelObj && viewModelObj.___$unmapCustom) {//Defer to customUnmapping where specified result = viewModelObj.___$unmapCustom(viewModelObj); } else if ((wasWrapped && isPrimativeOrDate(unwrapped)) || isNullOrUndefined(unwrapped) ) { //return null, undefined, values, and wrapped primativish values as is result = unwrapped; } else if (unwrapped instanceof Array) {//create new array to return and add unwrapped values to it result = []; for (p = 0, length = unwrapped.length; p < length; p++) { result[p] = recrusiveTo(unwrapped[p], { name: "[i]", parent: context.name + "[i]", full: context.full + "[i]" }); } } else if (unwrapped.constructor === Object) {//create new object to return and add unwrapped values to it result = {}; for (p in unwrapped) { if (p.substr(0, 4) !== "___$") {//ignore all properties starting with the magic string as internal if (viewModelObj.___$customChildren && viewModelObj.___$customChildren[p] && viewModelObj.___$customChildren[p].unmap) { result[p] = viewModelObj.___$customChildren[p].unmap(unwrapped[p]); } else { child = unwrapped[p]; if (!ko.isComputed(child) && !((temp = unwrap(child)) && temp.constructor === Function)) { recursiveResult = recrusiveTo(child, { name: p, parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, full: context.full + "." + p }); //if badResult wasn't returned then add property if (recursiveResult !== badResult) { result[p] = recursiveResult; } } } } } } else { //If it wasn't wrapped and it's not a function then return it. if (!wasWrapped && (typeof unwrapped !== "function")) { result = unwrapped; } } if (viewModelObj && viewModelObj.___$unmapExtend) {//if available call extend unmap function result = viewModelObj.___$unmapExtend(result, viewModelObj); } return result; } function recursiveUpdate(modelObj, viewModelObj, context, parentObj) { var p, q, found, foundModels, modelId, idName, length, unwrapped = unwrap(viewModelObj), wasWrapped = (viewModelObj !== unwrapped), child, map, tempArray, childTemp, childMap; if (fnLog) { fnLog(context);//log object being unmapped } if (wasWrapped && (isNullOrUndefined(unwrapped) ^ isNullOrUndefined(modelObj))) { //if you have an observable to update and either the new or old value is //null or undefined then update the observable viewModelObj(modelObj); } else if (modelObj && unwrapped && unwrapped.constructor == Object && modelObj.constructor === Object) { for (p in modelObj) {//loop through object properties and update them if (viewModelObj.___$customChildren && viewModelObj.___$customChildren[p]) { childMap = viewModelObj.___$customChildren[p].map || viewModelObj.___$customChildren[p]; unwrapped[p] = childMap(modelObj[p]); } else{ child = unwrapped[p]; if (!wasWrapped && unwrapped.hasOwnProperty(p) && (isPrimativeOrDate(child) || (child && child.constructor === Array))) { unwrapped[p] = modelObj[p]; } else if (child && typeof child.___$mapCustom === "function") { if (isObservable(child)) { childTemp = child.___$mapCustom(modelObj[p])//get child value mapped by custom maping childTemp = unwrap(childTemp);//don't nest observables... what you want is the value from the customMapping child(childTemp);//update child; } else {//property wasn't observable? update it anyway for return to server unwrapped[p] = unwrapped[p].___$mapCustom(modelObj[p]); } } else if (isNullOrUndefined(modelObj[p]) && unwrapped[p] && unwrapped[p].constructor === Object) { //Replace null or undefined with object for round trip to server; probably won't affect the view //WORKAROUND: If values are going to switch between obj and null/undefined and the UI needs to be updated //then the user should use the extend option to wrap the object in an observable unwrapped[p] = modelObj[p]; } else {//Recursive update everything else recursiveUpdate(modelObj[p], unwrapped[p], { name: p, parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, full: context.full + "." + p }, unwrapped); } } } } else if (unwrapped && unwrapped instanceof Array) { if (idName = viewModelObj.___$childIdName) {//id is specified, create, update, and delete by id foundModels = []; for (p = modelObj.length - 1; p >= 0; p--) { found = false; modelId = modelObj[p][idName]; for (q = unwrapped.length - 1; q >= 0; q--) { if (modelId === unwrapped[q][idName]()) {//If updated model id equals viewmodel id then update viewmodel object with model data recursiveUpdate(modelObj[p], unwrapped[q], { name: "[i]", parent: context.name + "[i]", full: context.full + "[i]" }); found = true; foundModels[q] = true; break; } } if (!found) {//If not found in updated model then remove from viewmodel viewModelObj.splice(p, 1); } } for (p = modelObj.length - 1; p >= 0; p--) { if (!foundModels[p]) {//If found and updated in viewmodel then add to viewmodel viewModelObj.pushFromModel(modelObj[p]); } } } else {//no id specified, replace old array items with new array items tempArray = []; map = viewModelObj.___$mapCustom; if (typeof map === "function") {//update array with mapped objects, use indexer for performance for (p = 0, length = modelObj.length; p < length; p++) { tempArray[p] = modelObj[p]; } viewModelObj(map(tempArray)); } else {//Can't use indexer for assignment; have to preserve original mapping with push viewModelObj(tempArray); for (p = 0, length = modelObj ? modelObj.length : 0; p < length; p++) { viewModelObj.pushFromModel(modelObj[p]); } } } } else if (wasWrapped) {//If it makes it this far and it was wrapped then update it viewModelObj(modelObj); } } function initInternals(options, startMessage) { makeChildArraysObservable = options.makeChildArraysObservable; if (window.console && options.logging) { //if logging should be done then log start message and add logging function console.log(startMessage); //Updates the console with information about what has been mapped and how fnLog = function fnUpdateConsole(context, pathSettings, settings) { var msg; if (pathSettings && pathSettings.settingType) {//if a setting will be used log it //message reads: SettingType FullPath (matched: path that was matched) msg = pathSettings.settingType + " " + context.full + " (matched: '" + ( (settings[context.full] ? context.full : "") || (settings[context.parent] ? context.parent : "") || (context.name) ) + "')"; } else {//log that default mapping was used for the path msg = "default " + context.full; } console.log("- " + msg); }; } else { fnLog = undefined;//setting the fn to undefined makes it easy to test if logging should be done } } ko.viewmodel = { options: { makeChildArraysObservable: true, logging: false }, fromModel: function fnFromModel(model, options) { var settings = getPathSettingsDictionary(options); initInternals(this.options, "Mapping From Model"); return recrusiveFrom(model, settings, rootContext); }, toModel: function fnToModel(viewmodel) { initInternals(this.options, "Mapping To Model"); return recrusiveTo(viewmodel, rootContext); }, updateFromModel: function fnUpdateFromModel(viewmodel, model) { initInternals(this.options, "Update From Model"); return recursiveUpdate(model, viewmodel, rootContext); } }; return ko.viewmodel; }))