├── Tests ├── updateFromModel-Mapping-qunit-tests.js ├── array-qunit-tests.js ├── TestForMappingCompatability.htm ├── null-Basic-qunit-tests.js ├── undefined-Basic-qunit-tests.js ├── issues-qunit-tests.js ├── fromModelToModel-Basic-qunit-tests.js ├── updateFromModel-Mapping-noncontiguous-qunit-tests.js ├── Tests.min.htm ├── Tests.htm ├── fromModelToModel-Mapping-qunit-tests.js ├── fromModel-Basic-qunit-tests.js ├── toModel-Basic-qunit-tests.js ├── simpleTypes-qunit-tests.js ├── null-Mapping-qunit-tests.js ├── undefined-Mapping-qunit-tests.js ├── nestedObject-qunit-tests.js ├── updateFromModel-Basic-qunit-tests.js └── fromModel-Mapping-qunit-tests.js ├── web ├── javascripts │ ├── main.js │ └── prism.js ├── images │ ├── bg_hr.png │ ├── blacktocat.png │ ├── icon_download.png │ └── sprite_download.png └── stylesheets │ ├── prism.css │ ├── pygment_trac.css │ └── stylesheet.css ├── nuget ├── 2.0.0 │ ├── knockout.viewmodel.2.0.0.nupkg │ ├── knockout.viewmodel.2.0.0.min.js │ └── knockout.viewmodel.2.0.0.js ├── 2.0.1 │ ├── knockout.viewmodel.2.0.1.nupkg │ ├── knockout.viewmodel.2.0.1.min.js │ └── knockout.viewmodel.2.0.1.js └── 2.0.2 │ ├── knockout.viewmodel.2.0.2.nupkg │ └── knockout.viewmodel.2.0.2.min.js ├── README.md ├── .gitignore ├── knockout.viewmodel.min.js ├── params.json └── index.html /Tests/updateFromModel-Mapping-qunit-tests.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/javascripts/main.js: -------------------------------------------------------------------------------- 1 | console.log('This would be the main JS file.'); 2 | -------------------------------------------------------------------------------- /web/images/bg_hr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderenaissance/knockout.viewmodel/HEAD/web/images/bg_hr.png -------------------------------------------------------------------------------- /web/images/blacktocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderenaissance/knockout.viewmodel/HEAD/web/images/blacktocat.png -------------------------------------------------------------------------------- /web/images/icon_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderenaissance/knockout.viewmodel/HEAD/web/images/icon_download.png -------------------------------------------------------------------------------- /web/images/sprite_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderenaissance/knockout.viewmodel/HEAD/web/images/sprite_download.png -------------------------------------------------------------------------------- /nuget/2.0.0/knockout.viewmodel.2.0.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderenaissance/knockout.viewmodel/HEAD/nuget/2.0.0/knockout.viewmodel.2.0.0.nupkg -------------------------------------------------------------------------------- /nuget/2.0.1/knockout.viewmodel.2.0.1.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderenaissance/knockout.viewmodel/HEAD/nuget/2.0.1/knockout.viewmodel.2.0.1.nupkg -------------------------------------------------------------------------------- /nuget/2.0.2/knockout.viewmodel.2.0.2.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderenaissance/knockout.viewmodel/HEAD/nuget/2.0.2/knockout.viewmodel.2.0.2.nupkg -------------------------------------------------------------------------------- /Tests/array-qunit-tests.js: -------------------------------------------------------------------------------- 1 | 2 | var model; 3 | module("toModel Basic", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | model = { 7 | array: [] 8 | }; 9 | }, 10 | teardown: function () { 11 | //ko.viewmodel.options.logging = false; 12 | model = undefined; 13 | } 14 | }); 15 | 16 | 17 | test("array push pop", function () { 18 | var viewmodel, result; 19 | 20 | viewmodel = ko.viewmodel.fromModel(model); 21 | viewmodel.array.push({ test: true }); 22 | result = viewmodel.array.pop(); 23 | 24 | assert(result.test(), true); 25 | 26 | }); 27 | 28 | test("array push pop", function () { 29 | var viewmodel, result; 30 | 31 | viewmodel = ko.viewmodel.fromModel(model); 32 | viewmodel.array.push({ test: true }, true); 33 | result = viewmodel.array.pop(); 34 | 35 | assert(result.test, true); 36 | 37 | }); 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The knockout viewmodel plugin, Copyright (c) 2012 Dave Herren 2 | 3 | The MIT License (MIT) 4 | ================== 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Tests/TestForMappingCompatability.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ko.viewmodel tests 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /web/stylesheets/prism.css: -------------------------------------------------------------------------------- 1 | /** 2 | * prism.js default theme for JavaScript, CSS and HTML 3 | * Based on dabblet (http://dabblet.com) 4 | * @author Lea Verou 5 | */ 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"] { 9 | color: black; 10 | text-shadow: 0 1px white; 11 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 12 | direction: ltr; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | 17 | -moz-tab-size: 4; 18 | -o-tab-size: 4; 19 | tab-size: 4; 20 | 21 | -webkit-hyphens: none; 22 | -moz-hyphens: none; 23 | -ms-hyphens: none; 24 | hyphens: none; 25 | } 26 | 27 | /* Code blocks */ 28 | pre[class*="language-"] { 29 | padding: 1em; 30 | margin: .5em 0; 31 | overflow: auto; 32 | } 33 | 34 | :not(pre) > code[class*="language-"], 35 | pre[class*="language-"] { 36 | background: #f5f2f0; 37 | } 38 | 39 | /* Inline code */ 40 | :not(pre) > code[class*="language-"] { 41 | padding: .1em; 42 | border-radius: .3em; 43 | } 44 | 45 | .token.comment, 46 | .token.prolog, 47 | .token.doctype, 48 | .token.cdata { 49 | color: slategray; 50 | } 51 | 52 | .token.punctuation { 53 | color: #999; 54 | } 55 | 56 | .namespace { 57 | opacity: .7; 58 | } 59 | 60 | .token.property, 61 | .token.tag, 62 | .token.boolean, 63 | .token.number { 64 | color: #905; 65 | } 66 | 67 | .token.selector, 68 | .token.attr-name, 69 | .token.string { 70 | color: #690; 71 | } 72 | 73 | .token.operator, 74 | .token.entity, 75 | .token.url, 76 | .language-css .token.string, 77 | .style .token.string { 78 | color: #a67f59; 79 | background: hsla(0,0%,100%,.5); 80 | } 81 | 82 | .token.atrule, 83 | .token.attr-value, 84 | .token.keyword { 85 | color: #07a; 86 | } 87 | 88 | 89 | .token.regex, 90 | .token.important { 91 | color: #e90; 92 | } 93 | 94 | .token.important { 95 | font-weight: bold; 96 | } 97 | 98 | .token.entity { 99 | cursor: help; 100 | } 101 | -------------------------------------------------------------------------------- /Tests/null-Basic-qunit-tests.js: -------------------------------------------------------------------------------- 1 | /// 2 | var model, updatedModel, modelResult; 3 | module("Basic Null Tests", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | 7 | model = { 8 | Prop1: null, 9 | Prop2: "test2", 10 | Prop3: null, 11 | Prop4: {}, 12 | Prop5: null, 13 | Prop6: [] 14 | }; 15 | 16 | updatedModel = { 17 | Prop1: "test2", 18 | Prop2: null, 19 | Prop3: {}, 20 | Prop4: null, 21 | Prop5: [], 22 | Prop6: null 23 | 24 | }; 25 | 26 | }, 27 | teardown: function () { 28 | //ko.viewmodel.options.logging = false; 29 | model = undefined; 30 | updatedModel = undefined; 31 | modelResult = undefined; 32 | } 33 | }); 34 | 35 | 36 | test("Basic", function () { 37 | 38 | var viewmodel = ko.viewmodel.fromModel(model); 39 | 40 | deepEqual(viewmodel.Prop1(), model.Prop1, "Null Prop Test");//1 41 | deepEqual(viewmodel.Prop2(), model.Prop2, "String Prop Test");//2 42 | deepEqual(viewmodel.Prop4, model.Prop4, "Object Prop Test");//3 43 | deepEqual(viewmodel.Prop6(), model.Prop6, "Array Prop Test");//4 44 | 45 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 46 | 47 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1, "Null to Value Update Test");//5 48 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2, "Value to Null Update Test");//6 49 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3, "Null to Object Update Test");//7 50 | deepEqual(viewmodel.Prop4, updatedModel.Prop4, "Object to Null Update Test");//8 51 | deepEqual(viewmodel.Prop5(), updatedModel.Prop5, "Null to Array Update Test");//9 52 | notEqual(typeof viewmodel.Prop5.push, "function", "Null to Array Update Is Not Observable Array Test");//10 53 | deepEqual(viewmodel.Prop6(), updatedModel.Prop6, "Array to Null Update Test");//11 54 | 55 | modelResult = ko.viewmodel.toModel(viewmodel); 56 | 57 | deepEqual(modelResult.Prop1, updatedModel.Prop1, "Null to Value Update Test");//12 58 | deepEqual(modelResult.Prop2, updatedModel.Prop2, "Value to Null Update Test");//13 59 | deepEqual(modelResult.Prop3, updatedModel.Prop3, "Null to Object Update Test");//14 60 | deepEqual(modelResult.Prop4, updatedModel.Prop4, "Object to Null Update Test");//15 61 | deepEqual(modelResult.Prop5, updatedModel.Prop5, "Null to Array Update Test");//16 62 | deepEqual(modelResult.Prop6, updatedModel.Prop6, "Array to Null Update Test");//17 63 | }); 64 | -------------------------------------------------------------------------------- /Tests/undefined-Basic-qunit-tests.js: -------------------------------------------------------------------------------- 1 | /// 2 | var model, updatedModel, modelResult; 3 | module("Basic Undefined Tests", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | 7 | model = { 8 | Prop1: undefined, 9 | Prop2: "test2", 10 | Prop3: undefined, 11 | Prop4: {}, 12 | Prop5: undefined, 13 | Prop6: [] 14 | }; 15 | 16 | updatedModel = { 17 | Prop1: "test2", 18 | Prop2: undefined, 19 | Prop3: {}, 20 | Prop4: undefined, 21 | Prop5: [], 22 | Prop6: undefined 23 | 24 | }; 25 | 26 | }, 27 | teardown: function () { 28 | //ko.viewmodel.options.logging = false; 29 | model = undefined; 30 | updatedModel = undefined; 31 | modelResult = undefined; 32 | } 33 | }); 34 | 35 | 36 | test("Basic", function () { 37 | 38 | var viewmodel = ko.viewmodel.fromModel(model); 39 | 40 | deepEqual(viewmodel.Prop1(), model.Prop1, "Undefined Prop Test");//1 41 | deepEqual(viewmodel.Prop2(), model.Prop2, "String Prop Test");//2 42 | deepEqual(viewmodel.Prop4, model.Prop4, "Object Prop Test");//3 43 | deepEqual(viewmodel.Prop6(), model.Prop6, "Array Prop Test");//4 44 | 45 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 46 | 47 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1, "Undefined to Value Update Test");//5 48 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2, "Value to Undefined Update Test");//6 49 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3, "Undefined to Object Update Test");//7 50 | deepEqual(viewmodel.Prop4, updatedModel.Prop4, "Object to Undefined Update Test");//8 51 | deepEqual(viewmodel.Prop5(), updatedModel.Prop5, "Undefined to Array Update Test");//9 52 | notEqual(typeof viewmodel.Prop5.push, "function", "Undefined to Array Update Is Not Observable Array Test");//10 53 | deepEqual(viewmodel.Prop6(), updatedModel.Prop6, "Array to Undefined Update Test");//11 54 | 55 | modelResult = ko.viewmodel.toModel(viewmodel); 56 | 57 | deepEqual(modelResult.Prop1, updatedModel.Prop1, "Undefined to Value Update Test");//12 58 | deepEqual(modelResult.Prop2, updatedModel.Prop2, "Value to Undefined Update Test");//13 59 | deepEqual(modelResult.Prop3, updatedModel.Prop3, "Undefined to Object Update Test");//14 60 | deepEqual(modelResult.Prop4, updatedModel.Prop4, "Object to Undefined Update Test");//15 61 | deepEqual(modelResult.Prop5, updatedModel.Prop5, "Undefined to Array Update Test");//16 62 | deepEqual(modelResult.Prop6, updatedModel.Prop6, "Array to Undefined Update Test");//17 63 | }); 64 | -------------------------------------------------------------------------------- /Tests/issues-qunit-tests.js: -------------------------------------------------------------------------------- 1 | /// 2 | var model, viewmodel, updatedModel, modelResult; 3 | module("Issue Tests", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | 7 | }, 8 | teardown: function () { 9 | //ko.viewmodel.options.logging = false; 10 | model = undefined; 11 | updatedModel = undefined; 12 | modelResult = undefined; 13 | viewmodel = undefined 14 | } 15 | }); 16 | 17 | 18 | test("Issue 19 - empty array throws an exception on update", function () { 19 | 20 | model = { items: [] }; 21 | 22 | updatedModel = { 23 | items: [{ 24 | id: 5, 25 | text: "test" 26 | }] 27 | }; 28 | 29 | viewmodel = ko.viewmodel.fromModel(model); 30 | 31 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 32 | 33 | deepEqual(viewmodel.items().length, 1); 34 | 35 | }); 36 | 37 | test("Issue 18 - toModel call fails to completely strip extended functions and internal properties", function () { 38 | 39 | model = { 40 | items: [{ 41 | id: 5, 42 | text: "test" 43 | }], 44 | obj: { 45 | text: "test" 46 | } 47 | }; 48 | 49 | viewmodel = ko.viewmodel.fromModel(model, { 50 | id: ["{root}.items[i].id"], 51 | extend: { 52 | "{root}.obj": function (obj) { 53 | obj.getTextLength = function () { 54 | return obj.text().length; 55 | }; 56 | 57 | obj.textLength = ko.computed(function () { 58 | return obj.text().length; 59 | }); 60 | 61 | } 62 | } 63 | }); 64 | 65 | modelResult = ko.viewmodel.toModel(viewmodel); 66 | 67 | deepEqual(modelResult.items[0].hasOwnProperty("..idName"), false, "hasOwnProperty of internal idName property returns true"); 68 | 69 | deepEqual(typeof modelResult.obj.getTextLength, "undefined", "function not removed"); 70 | deepEqual(modelResult.obj.hasOwnProperty("getTextLength"), false, "hasOwnProperty of function extension returns true"); 71 | 72 | deepEqual(typeof modelResult.obj.textLength, "undefined", "property extension not removed"); 73 | deepEqual(modelResult.obj.hasOwnProperty("textLength"), false, "hasOwnProperty of property extension returns true"); 74 | 75 | }); 76 | 77 | 78 | test("Issue 17 - toModel call fails to correctly unwrap fromModel with nested observableArray of strings", function () { 79 | 80 | model = { items: [["a", "b", "c", "d"]] }; 81 | 82 | viewmodel = ko.viewmodel.fromModel(model); 83 | 84 | modelResult = ko.viewmodel.toModel(viewmodel); 85 | 86 | deepEqual(model, modelResult); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /Tests/fromModelToModel-Basic-qunit-tests.js: -------------------------------------------------------------------------------- 1 | module("fromModel toModel Basic", { 2 | setup: function () { 3 | //ko.viewmodel.options.logging = true; 4 | }, 5 | teardown: function () { 6 | //ko.viewmodel.options.logging = false; 7 | } 8 | }); 9 | 10 | test("Default Basic Types", function () { 11 | var model, viewmodel, modelResult; 12 | 13 | model = { 14 | stringProp: "test", 15 | number: 5, 16 | date: new Date("01/01/2001"), 17 | emptyArray: [] 18 | }; 19 | 20 | viewmodel = ko.viewmodel.fromModel(model); 21 | 22 | modelResult = ko.viewmodel.toModel(viewmodel); 23 | 24 | deepEqual(modelResult.stringProp, model.stringProp, "String Test"); 25 | deepEqual(modelResult.number, model.number, "Number Test"); 26 | deepEqual(modelResult.date, model.date, "Date Test"); 27 | deepEqual(modelResult.emptyArray, model.emptyArray, "Array Test"); 28 | }); 29 | 30 | test("Default Nested Object", function () { 31 | var model, viewmodel, modelResult; 32 | 33 | model = { 34 | nestedObject: { 35 | stringProp: "test", 36 | number: 5, 37 | date: new Date("01/01/2001"), 38 | emptyArray: [] 39 | } 40 | }; 41 | 42 | viewmodel = ko.viewmodel.fromModel(model); 43 | 44 | modelResult = ko.viewmodel.toModel(viewmodel); 45 | 46 | deepEqual(modelResult.nestedObject.stringProp, model.nestedObject.stringProp, "String Test"); 47 | deepEqual(modelResult.nestedObject.number, model.nestedObject.number, "Number Test"); 48 | deepEqual(modelResult.nestedObject.date, model.nestedObject.date, "Date Test"); 49 | deepEqual(modelResult.nestedObject.emptyArray, model.nestedObject.emptyArray, "Array Test"); 50 | }); 51 | 52 | test("Default Object Array", function () { 53 | var model, viewmodel, modelResult; 54 | 55 | model = { 56 | objectArray: [ 57 | { 58 | stringProp: "test", 59 | number: 5, 60 | date: new Date("01/01/2001"), 61 | emptyArray: [] 62 | } 63 | ] 64 | }; 65 | 66 | viewmodel = ko.viewmodel.fromModel(model); 67 | 68 | modelResult = ko.viewmodel.toModel(viewmodel); 69 | 70 | deepEqual(modelResult.objectArray[0].stringProp, model.objectArray[0].stringProp, "String Test"); 71 | deepEqual(modelResult.objectArray[0].number, model.objectArray[0].number, "Number Test"); 72 | deepEqual(modelResult.objectArray[0].date, model.objectArray[0].date, "Date Test"); 73 | deepEqual(modelResult.objectArray[0].emptyArray, model.objectArray[0].emptyArray, "Array Test"); 74 | }); 75 | 76 | test("Default Nested Array", function () { 77 | var viewmodel, modelResult; 78 | 79 | viewmodel = ko.observable({ 80 | nestedArray: ko.observableArray([ko.observableArray([])]) 81 | }); 82 | 83 | modelResult = ko.viewmodel.toModel(viewmodel); 84 | 85 | deepEqual(modelResult.nestedArray[0], viewmodel().nestedArray()[0](), "Array Test"); 86 | }); 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | 165 | <<<<<<< HEAD 166 | # Proj Stuff 167 | My Project 168 | ======= 169 | #vs 170 | My Project 171 | My Project\* 172 | qunit* 173 | knockout.js 174 | *proj 175 | Properties 176 | *.config 177 | 178 | >>>>>>> origin/master 179 | -------------------------------------------------------------------------------- /Tests/updateFromModel-Mapping-noncontiguous-qunit-tests.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var model, updatedmodel, viewmodel, customMapping, originalChildObject, modelResult, actual, newChildObject; 3 | 4 | module("updateFromModel Noncontiguous Object Updates", { 5 | setup: function () { 6 | 7 | }, 8 | teardown: function () { 9 | 10 | } 11 | }); 12 | 13 | 14 | model = { items: [{ test: 5, Id: 3 }] }; 15 | 16 | var customMapping = { 17 | extend: { 18 | "{root}.items[i]": function (item) { 19 | return ko.observable(item); 20 | } 21 | }, 22 | arrayChildId: { 23 | "{root}.items": "Id" 24 | } 25 | }; 26 | 27 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 28 | 29 | originalChildObject = viewmodel.items()[0](); 30 | 31 | updatedmodel = { items: [{ test: 9, Id: 3 }] }; 32 | test("Contiguous Test Fail", function () { 33 | 34 | newChildObject = viewmodel.items()[0](); 35 | modelResult = ko.viewmodel.toModel(viewmodel); 36 | 37 | notEqual(viewmodel.items()[0]().test(), updatedmodel.items[0].test, "Test value has been updated in viewmodel"); 38 | }); 39 | 40 | 41 | test("Contiguous Test Pass", function () { 42 | ko.viewmodel.updateFromModel(viewmodel, updatedmodel); 43 | newChildObject = viewmodel.items()[0](); 44 | modelResult = ko.viewmodel.toModel(viewmodel); 45 | 46 | deepEqual(viewmodel.items()[0]().test(), updatedmodel.items[0].test, "Test value has been updated in viewmodel"); 47 | deepEqual(updatedmodel, modelResult, "Updated model and toModel have same values"); 48 | ok(originalChildObject == newChildObject, "Original child object was updated and not replaced."); 49 | }); 50 | 51 | }()); 52 | 53 | (function () { 54 | var model, updatedmodel, viewmodel, customMapping, originalChildObject, modelResult, actual, newChildObject; 55 | 56 | module("updateFromModel Noncontiguous Object Updates", { 57 | setup: function () { 58 | 59 | }, 60 | teardown: function () { 61 | 62 | } 63 | }); 64 | 65 | 66 | model = { items: [{ test: 5, Id: 3 }] }; 67 | 68 | var customMapping = { 69 | extend: { 70 | "{root}.items[i]": function (item) { 71 | return ko.observable(item); 72 | } 73 | }, 74 | arrayChildId: { 75 | "{root}.items": "Id" 76 | } 77 | }; 78 | 79 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 80 | 81 | originalChildObject = viewmodel.items()[0](); 82 | 83 | 84 | (function () { 85 | updatedmodel = { items: [{ test: 7, Id: 3 }] }; 86 | 87 | ko.viewmodel.updateFromModel(viewmodel, updatedmodel, true).onComplete(function () { 88 | asyncTest("Noncontiguous Test", function () { 89 | expect(4); 90 | newChildObject = viewmodel.items()[0](); 91 | modelResult = ko.viewmodel.toModel(viewmodel); 92 | 93 | deepEqual(viewmodel.items()[0]().test(), updatedmodel.items[0].test, "Test value has been updated in viewmodel"); 94 | deepEqual(viewmodel.items()[0]().test(), 7, "Test value is what was expected"); 95 | deepEqual(updatedmodel, modelResult, "Updated model and toModel have same values"); 96 | ok(originalChildObject == newChildObject, "Original child object was updated and not replaced."); 97 | start(); 98 | }); 99 | }); 100 | }()); 101 | 102 | }()); 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /nuget/2.0.0/knockout.viewmodel.2.0.0.min.js: -------------------------------------------------------------------------------- 1 | //v2.0.0 - C 2013, Dave Herren, License: MIT 2 | ko.viewmodel=function(){function m(b){return null===b||void 0===b}function v(b){return null===b||void 0===b||b.constructor===String||b.constructor===Number||b.constructor===Boolean||b instanceof Date}function n(b,f,d){var a,c,e,g,l,j,h=f?f[d.full]||f[d.parent]||f[d.name]||{}:{};k&&k(d,h,f);k&&k(d);if(a=h.custom)j=!0,"function"===typeof a?c=a(b):(c=a.map(b),m(c)||(c.___$mapCustom=a.map,a.unmap&&(c.___$unmapCustom=a.unmap)));else if(h.append)j=!0,m(b)||(b.___$appended=void 0),c=b;else{if(h.exclude)return j= 3 | !0,q;if(v(b))c=d.parentIsArray?b:x(b);else if(b instanceof Array){c=[];e=0;for(a=b.length;e 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Knockout Viewmodel Plugin Unit Tests 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | View on GitHub 24 | 25 |

Knockout Viewmodel Plugin

26 |

Cleaner, Faster, Better Knockout Mapping

27 | 49 |
50 | Download this project as a .zip file 51 | Download this project as a tar.gz file 52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
Download Latest Version
60 | (also on Nuget) 61 |
62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |
78 | 79 | 85 | 86 | -------------------------------------------------------------------------------- /Tests/Tests.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Knockout Viewmodel Plugin Unit Tests 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | View on GitHub 24 | 25 |

Knockout Viewmodel Plugin

26 |

Cleaner, Faster, Better Knockout Mapping

27 | 49 |
50 | Download this project as a .zip file 51 | Download this project as a tar.gz file 52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
Download Latest Version
60 | (also on Nuget) 61 |
62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 | 80 | 86 | 87 | -------------------------------------------------------------------------------- /nuget/2.0.1/knockout.viewmodel.2.0.1.min.js: -------------------------------------------------------------------------------- 1 | //v2.0.1 - C 2013, Dave Herren, License: MIT 2 | (function(){function w(a,g){var d=a?a[g.full]||a[g.parent]||a[g.name]||{}:{};k&&k(g,d,a);return d}function m(a){return null===a||void 0===a}function q(a){return null===a||void 0===a||a.constructor===String||a.constructor===Number||a.constructor===Boolean||a instanceof Date}function n(a,g,d,c){var e,b,f,l,j,h;c=c||w(g,d);var k;if(e=c.custom)h=!0,"function"===typeof e?b=e(a):(b=e.map(a),m(b)||(b.___$mapCustom=e.map,e.unmap&&(b.___$unmapCustom=e.unmap)));else if(c.append)h=!0,b=a;else{if(c.exclude)return h= 3 | !0,r;if(q(a))b=d.parentIsArray?a:y(a);else if(a instanceof Array){b=[];f=0;for(e=a.length;f 2 | module("fromModel toModel Mapping", { 3 | setup: function () { 4 | //ko.viewmodel.options.logging = true; 5 | }, 6 | teardown: function () { 7 | //ko.viewmodel.options.logging = false; 8 | } 9 | }); 10 | 11 | test("Extend full path", function () { 12 | var model, viewmodel, modelResult; 13 | 14 | model = { 15 | test: { 16 | stringProp: "test" 17 | } 18 | }; 19 | 20 | var customMapping = { 21 | extend: { 22 | "{root}.test.stringProp": function (obj) { 23 | obj.repeat = ko.computed(function () { 24 | return obj() + obj(); 25 | }); 26 | return obj; 27 | } 28 | } 29 | }; 30 | 31 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 32 | 33 | deepEqual(viewmodel().test().stringProp.repeat(), viewmodel().test().stringProp() + viewmodel().test().stringProp(), "Extension Added"); 34 | }); 35 | 36 | test("Extend object property path", function () { 37 | var model, viewmodel, modelResult; 38 | 39 | model = { 40 | test: { 41 | stringProp: "test" 42 | } 43 | }; 44 | 45 | var customMapping = { 46 | extend: { 47 | "test.stringProp": function (obj) { 48 | obj.repeat = ko.computed(function () { 49 | return obj() + obj(); 50 | }); 51 | return obj; 52 | } 53 | } 54 | }; 55 | 56 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 57 | 58 | deepEqual(viewmodel().test().stringProp.repeat(), viewmodel().test().stringProp() + viewmodel().test().stringProp(), "Extension Added"); 59 | }); 60 | 61 | test("Extend name path", function () { 62 | var model, viewmodel, modelResult; 63 | 64 | model = { 65 | stringProp: "test" 66 | }; 67 | 68 | var customMapping = { 69 | extend: { 70 | "stringProp": function (obj) { 71 | obj.repeat = ko.computed(function () { 72 | return obj() + obj(); 73 | }); 74 | return obj; 75 | } 76 | } 77 | }; 78 | 79 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 80 | 81 | deepEqual(viewmodel().stringProp.repeat(), viewmodel().stringProp() + viewmodel().stringProp(), "Extension Added"); 82 | }); 83 | 84 | 85 | test("Extend array item property", function () { 86 | var model, viewmodel, modelResult, actual, expected; 87 | 88 | model = { 89 | items: [{ 90 | test: { 91 | stringProp: "test" 92 | } 93 | }] 94 | }; 95 | 96 | var customMapping = { 97 | extend: { 98 | "items[i].test": function (obj) { 99 | obj.repeat = ko.computed(function () { 100 | return obj().stringProp() + obj().stringProp(); 101 | }); 102 | return obj; 103 | } 104 | } 105 | }; 106 | 107 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 108 | 109 | actual = viewmodel().items()[0]().test.repeat(); 110 | expected = model.items[0].test.stringProp + model.items[0].test.stringProp; 111 | 112 | deepEqual(actual, expected); 113 | }); 114 | 115 | test("Exclude property", function () { 116 | var model, viewmodel, modelResult, actual, expected; 117 | 118 | model = { 119 | items: [{ 120 | test: { 121 | stringProp: "test" 122 | } 123 | }] 124 | }; 125 | 126 | var options = { 127 | exclude: ["items[i].test"] 128 | }; 129 | 130 | viewmodel = ko.viewmodel.fromModel(model, options); 131 | 132 | modelResult = ko.viewmodel.toModel(viewmodel); 133 | 134 | deepEqual(viewmodel().items()[0]().test, undefined, "fromModel assert"); 135 | deepEqual(modelResult.items[0].test, undefined, "toModel assert"); 136 | }); 137 | 138 | test("Append property", function () { 139 | var model, viewmodel, modelResult, actual, expected; 140 | 141 | model = { 142 | items: [{ 143 | test: { 144 | stringProp: "test" 145 | } 146 | }] 147 | }; 148 | 149 | var customMapping = { 150 | append: ["items[i]"] 151 | }; 152 | 153 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 154 | modelResult = ko.viewmodel.toModel(viewmodel); 155 | 156 | deepEqual(viewmodel().items()[0].test.stringProp, model.items[0].test.stringProp, "fromModel assert"); 157 | deepEqual(modelResult.items[0].test.stringProp, model.items[0].test.stringProp, "toModel assert"); 158 | }); 159 | 160 | -------------------------------------------------------------------------------- /web/javascripts/prism.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 4 | * @author Lea Verou http://lea.verou.me 5 | */(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(//g,">").replace(/\u00a0/g," ");var l={element:r,language:o,grammar:u,code:f};t.hooks.run("before-highlight",l);if(i&&self.Worker){var c=new Worker(t.filename);c.onmessage=function(e){l.highlightedCode=n.stringify(JSON.parse(e.data));l.element.innerHTML=l.highlightedCode;s&&s.call(l.element);t.hooks.run("after-highlight",l)};c.postMessage(JSON.stringify({language:l.language,code:l.code}))}else{l.highlightedCode=t.highlight(l.code,l.grammar);l.element.innerHTML=l.highlightedCode;s&&s.call(r);t.hooks.run("after-highlight",l)}},highlight:function(e,r){return n.stringify(t.tokenize(e,r))},tokenize:function(e,n){var r=t.Token,i=[e],s=n.rest;if(s){for(var o in s)n[o]=s[o];delete n.rest}e:for(var o in n){if(!n.hasOwnProperty(o)||!n[o])continue;var u=n[o],a=u.inside,f=!!u.lookbehind||0;u=u.pattern||u;for(var l=0;le.length)break e;if(c instanceof r)continue;u.lastIndex=0;var h=u.exec(c);if(h){f&&(f=h[1].length);var p=h.index-1+f,h=h[0].slice(f),d=h.length,v=p+d,m=c.slice(0,p+1),g=c.slice(v+1),y=[l,1];m&&y.push(m);var b=new r(o,a?t.tokenize(h,a):h);y.push(b);g&&y.push(g);Array.prototype.splice.apply(i,y)}}}return i},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]"){for(var r=0;r"+i.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; 6 | Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,number:/\b-?(0x)?\d*\.?[\da-f]+\b/g,operator:/[-+]{1,2}|!|=?<|=?>|={1,2}|(&){1,2}|\|?\||\?|\*|\//g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g};; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|new|with|typeof|try|catch|finally|null|break|continue)\b/g,number:/\b(-?(0x)?\d*\.?[\da-f]+|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; 8 | -------------------------------------------------------------------------------- /knockout.viewmodel.min.js: -------------------------------------------------------------------------------- 1 | -//v2.0.3 - C 2013, Dave Herren, License: MIT 2 | (function(){function y(b,g){var e=b?b[g.full]||b[g.parent]||b[g.name]||{}:{};p&&p(g,e,b);return e}function r(b){return null===b||void 0===b}function u(b){return null===b||void 0===b||b.constructor===String||b.constructor===Number||b.constructor===Boolean||b instanceof Date}function n(b,g,e,c){var f,a,d,h,k,l,m;c=c||y(g,e);if(f=c.custom)l=!0,"function"===typeof f?(a=f(b),r(a)||(a.___$mapCustom=f)):(a=f.map(b),r(a)||(a.___$mapCustom=f.map,f.unmap&&(a.___$unmapCustom=f.unmap)));else if(c.append)l=!0, 3 | a=b;else{if(c.exclude)return l=!0,v;if(u(b))a=e.parentIsArray?b:B(b);else if(b instanceof Array){a=[];d=0;for(f=b.length;d 2 | module("fromModel Basic", { 3 | setup: function () { 4 | //ko.viewmodel.options.logging = true; 5 | if(ko.viewmodel.options.hasOwnProperty("makeChildArraysObservable")){ 6 | ko.viewmodel.options.makeChildArraysObservable = true; 7 | } 8 | }, 9 | teardown: function () { 10 | //ko.viewmodel.options.logging = false; 11 | } 12 | }); 13 | 14 | 15 | test("Default simple types", function () { 16 | var model, viewmodel; 17 | 18 | model = { 19 | stringProp: "test", 20 | number: 5, 21 | date: new Date("01/01/2001"), 22 | emptyArray: [] 23 | }; 24 | 25 | viewmodel = ko.viewmodel.fromModel(model); 26 | 27 | deepEqual(viewmodel.stringProp(), model.stringProp, "String Test"); 28 | deepEqual(viewmodel.number(), model.number, "Number Test"); 29 | deepEqual(viewmodel.date(), model.date, "Date Test"); 30 | deepEqual(viewmodel.emptyArray(), model.emptyArray, "Array Test"); 31 | }); 32 | 33 | 34 | test("Default nested object", function () { 35 | var model, viewmodel; 36 | 37 | model = { 38 | nestedObject: { 39 | stringProp: "test", 40 | number: 5, 41 | date: new Date("01/01/2001"), 42 | emptyArray: [] 43 | } 44 | }; 45 | 46 | viewmodel = ko.viewmodel.fromModel(model); 47 | 48 | deepEqual(viewmodel.nestedObject.stringProp(), model.nestedObject.stringProp, "String Test"); 49 | deepEqual(viewmodel.nestedObject.number(), model.nestedObject.number, "Number Test"); 50 | deepEqual(viewmodel.nestedObject.date(), model.nestedObject.date, "Date Test"); 51 | deepEqual(viewmodel.nestedObject.emptyArray(), model.nestedObject.emptyArray, "Array Test"); 52 | }); 53 | 54 | 55 | test("Default object array", function () { 56 | var model, viewmodel; 57 | 58 | model = { 59 | objectArray: [ 60 | {} 61 | ] 62 | }; 63 | 64 | viewmodel = ko.viewmodel.fromModel(model); 65 | 66 | deepEqual(viewmodel.objectArray()[0], model.objectArray[0], "Object Test"); 67 | }); 68 | 69 | 70 | test("Default object array simple types", function () { 71 | var model, viewmodel; 72 | 73 | model = { 74 | objectArray: [ 75 | { 76 | stringProp: "test", 77 | number: 5, 78 | date: new Date("01/01/2001"), 79 | emptyArray: [] 80 | } 81 | ] 82 | }; 83 | 84 | viewmodel = ko.viewmodel.fromModel(model); 85 | 86 | deepEqual(viewmodel.objectArray()[0].stringProp(), model.objectArray[0].stringProp, "String Test"); 87 | deepEqual(viewmodel.objectArray()[0].number(), model.objectArray[0].number, "Number Test"); 88 | deepEqual(viewmodel.objectArray()[0].date(), model.objectArray[0].date, "Date Test"); 89 | deepEqual(viewmodel.objectArray()[0].emptyArray(), model.objectArray[0].emptyArray, "Array Test"); 90 | }); 91 | 92 | 93 | test("makeChildArraysObservable is false Mode Default nested array", function () { 94 | var model, viewmodel; 95 | 96 | model = { 97 | nestedArray: [[]] 98 | }; 99 | 100 | ko.viewmodel.options.makeChildArraysObservable = false; 101 | viewmodel = ko.viewmodel.fromModel(model); 102 | 103 | deepEqual(ko.viewmodel.options.makeChildArraysObservable, false); 104 | deepEqual(viewmodel.nestedArray()[0], model.nestedArray[0], "Array Test"); 105 | }); 106 | 107 | test("makeChildArraysObservable is false double nested array", function () { 108 | var model, viewmodel; 109 | 110 | model = { 111 | nestedArray: [[[]]] 112 | }; 113 | 114 | ko.viewmodel.options.makeChildArraysObservable = false; 115 | viewmodel = ko.viewmodel.fromModel(model); 116 | 117 | deepEqual(ko.viewmodel.options.makeChildArraysObservable, false); 118 | deepEqual(viewmodel.nestedArray()[0][0], model.nestedArray[0][0], "Array Test"); 119 | }); 120 | 121 | //if undefined then we are using facade around ko.mapping 122 | //used to exclude tests that are incompatable with ko.mapping 123 | if (ko.viewmodel.options.makeChildArraysObservable !== undefined) { 124 | test("makeChildArraysObservable is true Default nested array", function () { 125 | var model, viewmodel; 126 | 127 | model = { 128 | nestedArray: [[]] 129 | }; 130 | 131 | viewmodel = ko.viewmodel.fromModel(model); 132 | 133 | deepEqual(ko.viewmodel.options.makeChildArraysObservable, true); 134 | deepEqual(viewmodel.nestedArray()[0](), model.nestedArray[0], "Array Test"); 135 | }); 136 | 137 | test("makeChildArraysObservable is true double nested array", function () { 138 | var model, viewmodel; 139 | 140 | model = { 141 | nestedArray: [[[]]] 142 | }; 143 | 144 | viewmodel = ko.viewmodel.fromModel(model); 145 | 146 | deepEqual(ko.viewmodel.options.makeChildArraysObservable, true); 147 | deepEqual(viewmodel.nestedArray()[0]()[0](), model.nestedArray[0][0], "Array Test"); 148 | }); 149 | } 150 | 151 | 152 | test("Default string array", function () { 153 | var model, viewmodel; 154 | 155 | model = { 156 | stringArray: ["Test", "Test"] 157 | }; 158 | 159 | viewmodel = ko.viewmodel.fromModel(model); 160 | 161 | deepEqual(viewmodel.stringArray()[0], model.stringArray[0], "String Array Test"); 162 | }); 163 | 164 | 165 | -------------------------------------------------------------------------------- /params.json: -------------------------------------------------------------------------------- 1 | {"note":"Don't delete this file! It's used internally to help with page regeneration.","name":"Knockout.viewmodel","body":"## Welcome to the knockout viewmodel plugin.\r\nThe knockout viewmodel plugin let's you create complex observable viewmodels easily and with more structure and control than ever before.\r\n\r\n### Example: Creating a simple viewmodel with ko.viewmodel\r\nCreating a viewmodel from a model is simple.\r\n```\r\nvar model, viewmodel;\r\n\r\n//Normally this JSON model would be returned via ajax.\r\nmodel = {\r\n users:[ \r\n\t\t{firstName:\"John\", lastName:\"Doe\"},\r\n\t\t{firstName:\"James\", lastName:\"Smith\"}\r\n\t]\r\n};\r\n\r\nviewmodel = ko.viewmodel.fromModel(model);\r\n```\r\nThis code creates an observable viewModel with an observable array of users whose properties are observable.\r\n\r\n### Example: Creating a viewmodel with ko.viewModel\r\nNow lets say that you want to extend each object in the users array with an isDeleted flag. You would do this by specifying custom mapping options.\r\n```\r\noptions:{ \r\n\textend:{\r\n\t\t\"{root}.users[i]\": function(user){\r\n\t\t\t//note that user is an observable\r\n\t\t\t//we're adding isDeleted to the object not the observable\r\n\t\t\tuser().isDeleted = ko.observable(false);\r\n\t\t}\r\n\t}\r\n};\r\n\r\nviewmodel = ko.viewmodel.fromModel(model,options));\r\n```\r\nWe've now extended each object in the users array with an isDeleted flag. With ko.viewmodel all objects and properties are observable by default so it's up to you whether you extend the object or observable. \r\n\r\nIf we also wanted to add a delete method to an array we would use the following options:\r\n```\r\noptions:{ \r\n\textend:{\r\n \"{root}.users[i]\": function(user){\r\n user().isDeleted= ko.observable(false);\r\n },\r\n \"{root}.users\": function(users){\r\n users.Delete = function(user){\r\n user.isDeleted(true);\r\n }\r\n }\r\n }\r\n};\r\n```\r\nNote we've extending the observable array with the Delete function. Here's a jsFiddle that shows the code bound to a view: http://jsfiddle.net/CodeRenaissance/ytjns/3/\r\n\r\n### Custom Processing\r\nEvery property can have custom processing specified for it. Custom processing is specified for an item by it's path. \r\n\r\n### Path Types\r\nEvery full path starts with {root}. Items in an array are referred to as [i]. So a path of \"{root}.users[i].firstName\" would be used to specify custom processing for the firstName property of every object in the users array. That is its full path name, but it's not necessary to refer to every item by it's -full path name. There are three ways of referencing a path:\r\n\r\n* Full Path Name - Matches only the specific path, e.g. \"{root}.users[i].firstName\".\r\n* Parent Child Name - Matches the parent child combination specified. So \"users[i].firstName\" will only match the first name property on objects in the users array. Since this is an array we can also specify this as \"[i].firstName\" which would match every firstName property on an array child.\r\n* Property Name - Matches every property with that name. So a partial path of \"firstName\" would match every firstName property in your model.\r\n\r\n\r\n### Processing Types\r\nThere four types of custom processing: map, append, exclude, allow, and extend.\r\n\r\n* map - the path and all of it's children are processed only as you specify\r\n* append - the path and it's children are appended as is \r\n* exclude - the path is excluded from processing\r\n* allow - the path is not processed but all of it's children are\r\n* extend - the path and it's children are processed normally but then extended/modified as specified\r\n\r\nAll processing types are exclusive. Processing types have the following order of operations map, append, exclude, allow, and extend. All options can be specified both for fromModel and toModel.\r\n\r\n### The map processing option\r\n\tWith the map processing option the path and all of it's children are processed only as you specify.\r\n\t```\r\n\toptions:{ \r\n\t\textend:{\r\n\t\t\t\"{root}.users[i]\": function(user){\r\n\t\t\t\tuser.isDeleted= ko.observable(false);\r\n\t\t\t\treturn user;\r\n\t\t\t}\r\n\t\t}\r\n\t};\r\n\t```\r\n\tIn this case the users are passed through as is with the addition of an obeservable isDeleted flag. \r\n\tNote: \r\n\t*Make sure to return something from your function, otherwise undefined will be returned as the result of map.\r\n\t*The object passed into map is the unaltered object from your viewmodel or model (depending on if you are calling fromModel or toModel).\r\n### The append processing option\r\n\tWith the append processing option the path and all of it's children are appended as is.\r\n\t```\r\n\toptions:{ \r\n\t\tappend:[\"{root}.users[i]\"]\r\n\t};\r\n\t```\r\n\tIn this case none of the users have been altered and are appended unchanged.\r\n### The exclude processing option\r\n\tWith the exclude processing option the path and it's children are excluded from processing and will not be included.\r\n\t```\r\n\toptions:{ \r\n\t\texclude:[\"{root}.users[i].firstName\"]\r\n\t};\r\n\t```\r\n\tThe firstName property is not included.\r\n\t\r\n### The override processing option\r\n\tThe override processing option allows some default processing to be overridden. It does not affect processing of children. It is meant to be a quick way to do common things that map would otherwise need to be used for.\r\n\t```\r\n\toptions:{ \r\n\t\toverride:[\"{root}.someProperty\"]\r\n\t};\r\n\t```\r\n\tWhen calling:\r\n\t*fromModel\r\n\t\t** the path specified will not be wrapped in an observable.\r\n\t*toModel:\r\n\t\t** if the path is for a computed value it will be unwrapped.\r\n\t\t** if the path is for a non-observable value it will be included.\r\n\t\t\r\n### The extend processing option\r\n\tWith the override processing option the path and it's children are processed normally but then extended/modified as specified.\r\n\t```\r\n\toptions:{ \r\n\t\textend:{\r\n\t\t\t\"{root}.users[i]\": function(user){\r\n\t\t\t\t//note that user is an observable\r\n\t\t\t\t//we're adding isDeleted to the object not the observable\r\n\t\t\t\tuser().isDeleted = ko.observable(false);\r\n\t\t\t}\r\n\t\t}\r\n\t};\r\n\t```\r\n\tThe value of the path is passed to the extend function after processing has been completed on the path and its children. Objects can be modified without being returned. \r\n\tNote: Whatever is returned from the extend function will take the place the processed object.","tagline":"","google":""} -------------------------------------------------------------------------------- /Tests/toModel-Basic-qunit-tests.js: -------------------------------------------------------------------------------- 1 | module("toModel Basic", { 2 | setup: function () { 3 | //ko.viewmodel.options.logging = true; 4 | }, 5 | teardown: function () { 6 | //ko.viewmodel.options.logging = false; 7 | } 8 | }); 9 | 10 | 11 | test("Default Basic Types", function () { 12 | var viewmodel, modelResult; 13 | 14 | viewmodel = { 15 | stringProp: ko.observable("test"), 16 | number: ko.observable(5), 17 | date: ko.observable(new Date("01/01/2001")), 18 | emptyArray: ko.observableArray([]) 19 | }; 20 | 21 | modelResult = ko.viewmodel.toModel(viewmodel); 22 | 23 | deepEqual(modelResult.stringProp, viewmodel.stringProp(), "String Test"); 24 | deepEqual(modelResult.number, viewmodel.number(), "Number Test"); 25 | deepEqual(modelResult.date, viewmodel.date(), "Date Test"); 26 | deepEqual(modelResult.emptyArray, viewmodel.emptyArray(), "Array Test"); 27 | }); 28 | 29 | test("Default Nested Object", function () { 30 | var viewmodel, modelResult; 31 | 32 | viewmodel = { 33 | nestedObject: { 34 | stringProp: ko.observable("test"), 35 | number: ko.observable(5), 36 | date: ko.observable(new Date("01/01/2001")), 37 | emptyArray: ko.observable([]) 38 | } 39 | }; 40 | 41 | modelResult = ko.viewmodel.toModel(viewmodel); 42 | 43 | deepEqual(modelResult.nestedObject.stringProp, viewmodel.nestedObject.stringProp(), "String Test"); 44 | deepEqual(modelResult.nestedObject.number, viewmodel.nestedObject.number(), "Number Test"); 45 | deepEqual(modelResult.nestedObject.date, viewmodel.nestedObject.date(), "Date Test"); 46 | deepEqual(modelResult.nestedObject.emptyArray, viewmodel.nestedObject.emptyArray(), "Array Test"); 47 | }); 48 | 49 | test("Default Object Array", function () { 50 | var viewmodel, modelResult; 51 | 52 | viewmodel = { 53 | objectArray: ko.observableArray([ 54 | { 55 | stringProp: ko.observable("test"), 56 | number: ko.observable(5), 57 | date: ko.observable(new Date("01/01/2001")), 58 | emptyArray: ko.observableArray([]) 59 | } 60 | ]) 61 | }; 62 | 63 | modelResult = ko.viewmodel.toModel(viewmodel); 64 | 65 | deepEqual(modelResult.objectArray[0].stringProp, viewmodel.objectArray()[0].stringProp(), "String Test"); 66 | deepEqual(modelResult.objectArray[0].number, viewmodel.objectArray()[0].number(), "Number Test"); 67 | deepEqual(modelResult.objectArray[0].date, viewmodel.objectArray()[0].date(), "Date Test"); 68 | deepEqual(modelResult.objectArray[0].emptyArray, viewmodel.objectArray()[0].emptyArray(), "Array Test"); 69 | }); 70 | 71 | test("Default Nested Array", function () { 72 | var viewmodel, modelResult; 73 | 74 | viewmodel = { 75 | nestedArray: ko.observableArray([ko.observableArray([])]) 76 | }; 77 | 78 | modelResult = ko.viewmodel.toModel(viewmodel); 79 | 80 | deepEqual(modelResult.nestedArray[0], viewmodel.nestedArray()[0](), "Array Test"); 81 | }); 82 | 83 | 84 | 85 | 86 | test("Observable Object Basic Types", function () { 87 | var viewmodel, modelResult; 88 | 89 | viewmodel = ko.observable({ 90 | stringProp: ko.observable("test"), 91 | number: ko.observable(5), 92 | date: ko.observable(new Date("01/01/2001")), 93 | emptyArray: ko.observableArray([]) 94 | }); 95 | 96 | modelResult = ko.viewmodel.toModel(viewmodel); 97 | 98 | deepEqual(modelResult.stringProp, viewmodel().stringProp(), "String Test"); 99 | deepEqual(modelResult.number, viewmodel().number(), "Number Test"); 100 | deepEqual(modelResult.date, viewmodel().date(), "Date Test"); 101 | deepEqual(modelResult.emptyArray, viewmodel().emptyArray(), "Array Test"); 102 | }); 103 | 104 | test("Observable Object Nested Object", function () { 105 | var viewmodel, modelResult; 106 | 107 | viewmodel = ko.observable({ 108 | nestedObject: ko.observable({ 109 | stringProp: ko.observable("test"), 110 | number: ko.observable(5), 111 | date: ko.observable(new Date("01/01/2001")), 112 | emptyArray: ko.observable([]) 113 | }) 114 | }); 115 | 116 | modelResult = ko.viewmodel.toModel(viewmodel); 117 | 118 | deepEqual(modelResult.nestedObject.stringProp, viewmodel().nestedObject().stringProp(), "String Test"); 119 | deepEqual(modelResult.nestedObject.number, viewmodel().nestedObject().number(), "Number Test"); 120 | deepEqual(modelResult.nestedObject.date, viewmodel().nestedObject().date(), "Date Test"); 121 | deepEqual(modelResult.nestedObject.emptyArray, viewmodel().nestedObject().emptyArray(), "Array Test"); 122 | }); 123 | 124 | test("Observable Object Object Array", function () { 125 | var viewmodel, modelResult; 126 | 127 | viewmodel = ko.observable({ 128 | objectArray: ko.observableArray([ 129 | ko.observable({ 130 | stringProp: ko.observable("test"), 131 | number: ko.observable(5), 132 | date: ko.observable(new Date("01/01/2001")), 133 | emptyArray: ko.observableArray([]) 134 | }) 135 | ]) 136 | }); 137 | 138 | modelResult = ko.viewmodel.toModel(viewmodel); 139 | 140 | deepEqual(modelResult.objectArray[0].stringProp, viewmodel().objectArray()[0]().stringProp(), "String Test"); 141 | deepEqual(modelResult.objectArray[0].number, viewmodel().objectArray()[0]().number(), "Number Test"); 142 | deepEqual(modelResult.objectArray[0].date, viewmodel().objectArray()[0]().date(), "Date Test"); 143 | deepEqual(modelResult.objectArray[0].emptyArray, viewmodel().objectArray()[0]().emptyArray(), "Array Test"); 144 | }); 145 | 146 | test("Observable Object Nested Array", function () { 147 | var viewmodel, modelResult; 148 | 149 | viewmodel = ko.observable({ 150 | nestedArray: ko.observableArray([ko.observableArray([])]) 151 | }); 152 | 153 | modelResult = ko.viewmodel.toModel(viewmodel); 154 | 155 | deepEqual(modelResult.nestedArray[0], viewmodel().nestedArray()[0](), "Array Test"); 156 | }); 157 | 158 | 159 | -------------------------------------------------------------------------------- /Tests/simpleTypes-qunit-tests.js: -------------------------------------------------------------------------------- 1 | /// 2 | var model, updatedModel, modelResult; 3 | module("Simple Types", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | 7 | model = { 8 | stringProp: "test", 9 | id: 5, 10 | date: new Date("01/01/2001") 11 | }; 12 | 13 | updatedModel = { 14 | stringProp: "test2", 15 | id: 6, 16 | date: new Date("02/01/2002") 17 | }; 18 | 19 | }, 20 | teardown: function () { 21 | //ko.viewmodel.options.logging = false; 22 | model = undefined; 23 | updatedModel = undefined; 24 | modelResult = undefined; 25 | } 26 | }); 27 | 28 | 29 | test("Basic", function () { 30 | 31 | var viewmodel = ko.viewmodel.fromModel(model); 32 | 33 | deepEqual(viewmodel.stringProp(), model.stringProp, "From Model String Test"); 34 | deepEqual(viewmodel.id(), model.id, "From Model Number Test"); 35 | deepEqual(viewmodel.date(), model.date, "From Model Date Test"); 36 | 37 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 38 | 39 | deepEqual(viewmodel.stringProp(), updatedModel.stringProp, "Update String Test"); 40 | deepEqual(viewmodel.id(), updatedModel.id, "Update Number Test"); 41 | deepEqual(viewmodel.date(), updatedModel.date, "Update Date Test"); 42 | 43 | modelResult = ko.viewmodel.toModel(viewmodel); 44 | 45 | deepEqual(modelResult, updatedModel, "Result Object Comparison"); 46 | }); 47 | 48 | test("Extend", function () { 49 | 50 | var viewmodel = ko.viewmodel.fromModel(model, { 51 | extend: { 52 | "{root}": function(root){ 53 | root.isValid = ko.computed(function () { 54 | return root.stringProp.isValid() && root.id.isValid() && root.date.isValid(); 55 | }); 56 | }, 57 | "{root}.stringProp": function (stringProp) { 58 | stringProp.isValid = ko.computed(function () { 59 | return stringProp() && stringProp().length; 60 | }); 61 | }, 62 | "{root}.id": function (id) { 63 | id.isValid = ko.computed(function () { 64 | return id() && id() > 0; 65 | }); 66 | }, 67 | "{root}.date": function (date) { 68 | date.isValid = ko.computed(function () { 69 | return date() && date() < new Date(); 70 | }); 71 | } 72 | } 73 | }); 74 | 75 | deepEqual(viewmodel.stringProp(), model.stringProp, "From Model String Test"); 76 | deepEqual(viewmodel.id(), model.id, "From Model Number Test"); 77 | deepEqual(viewmodel.date(), model.date, "From Model Date Test"); 78 | deepEqual(viewmodel.isValid(), true, "Extension check"); 79 | 80 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 81 | 82 | deepEqual(viewmodel.stringProp(), updatedModel.stringProp, "Update String Test"); 83 | deepEqual(viewmodel.id(), updatedModel.id, "Update Number Test"); 84 | deepEqual(viewmodel.date(), updatedModel.date, "Update Date Test"); 85 | deepEqual(viewmodel.isValid(), true, "Extension check"); 86 | 87 | modelResult = ko.viewmodel.toModel(viewmodel); 88 | 89 | deepEqual(viewmodel.isValid(), true, "Extension check"); 90 | }); 91 | 92 | test("Append root", function () { 93 | 94 | var viewmodel = ko.viewmodel.fromModel(model, { 95 | append: ["{root}"] 96 | }); 97 | 98 | deepEqual(viewmodel, model, "From Model Test"); 99 | 100 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 101 | 102 | notEqual(viewmodel, updatedModel, "Update Fail Test"); 103 | 104 | modelResult = ko.viewmodel.toModel(viewmodel); 105 | 106 | deepEqual(modelResult, model, "Result"); 107 | }); 108 | 109 | test("Append property", function () { 110 | 111 | var viewmodel = ko.viewmodel.fromModel(model, { 112 | append: ["{root}.stringProp"] 113 | }); 114 | 115 | deepEqual(viewmodel.stringProp, model.stringProp, "From Model String Test"); 116 | deepEqual(viewmodel.id(), model.id, "From Model Number Test"); 117 | deepEqual(viewmodel.date(), model.date, "From Model Date Test"); 118 | 119 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 120 | 121 | deepEqual(viewmodel.stringProp, updatedModel.stringProp, "From Model String Test Fail"); 122 | deepEqual(viewmodel.id(), updatedModel.id, "Update Number Test"); 123 | deepEqual(viewmodel.date(), updatedModel.date, "Update Date Test"); 124 | 125 | modelResult = ko.viewmodel.toModel(viewmodel); 126 | 127 | deepEqual(modelResult.stringProp, updatedModel.stringProp, "To Model String Test"); 128 | deepEqual(modelResult.id, updatedModel.id, "To Model Number Test"); 129 | deepEqual(modelResult.date, updatedModel.date, "To Model Date Test"); 130 | }); 131 | 132 | test("Custom basic", function () { 133 | 134 | var viewmodel = ko.viewmodel.fromModel(model, { 135 | custom: { 136 | "{root}.date":function (date) { 137 | return date.valueOf(); 138 | } 139 | } 140 | }); 141 | 142 | deepEqual(viewmodel.stringProp(), model.stringProp, "From Model String Test"); 143 | deepEqual(viewmodel.id(), model.id, "From Model Number Test"); 144 | deepEqual(viewmodel.date, model.date.valueOf(), "From Model Date Test"); 145 | 146 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 147 | 148 | deepEqual(viewmodel.stringProp(), updatedModel.stringProp, "Update String Test"); 149 | deepEqual(viewmodel.id(), updatedModel.id, "Update Number Test"); 150 | deepEqual(viewmodel.date, updatedModel.date.valueOf(), "Update Date Test"); 151 | 152 | modelResult = ko.viewmodel.toModel(viewmodel); 153 | 154 | deepEqual(modelResult.stringProp, updatedModel.stringProp, "To Model String Test"); 155 | deepEqual(modelResult.id, updatedModel.id, "To Model Number Test"); 156 | deepEqual(modelResult.date, updatedModel.date.valueOf(), "To Model Date Test"); 157 | }); 158 | 159 | test("Custom map and unmap", function () { 160 | 161 | var viewmodel = ko.viewmodel.fromModel(model, { 162 | custom: { 163 | "{root}.date":{ 164 | map: function (date) { 165 | return ko.observable(date.valueOf()); 166 | }, 167 | unmap: function(date){ 168 | return new Date(date()); 169 | } 170 | } 171 | } 172 | }); 173 | 174 | deepEqual(viewmodel.stringProp(), model.stringProp, "From Model String Test"); 175 | deepEqual(viewmodel.id(), model.id, "From Model Number Test"); 176 | deepEqual(viewmodel.date(), model.date.valueOf(), "From Model Date Test"); 177 | 178 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 179 | 180 | deepEqual(viewmodel.stringProp(), updatedModel.stringProp, "Update String Test"); 181 | deepEqual(viewmodel.id(), updatedModel.id, "Update Number Test"); 182 | deepEqual(viewmodel.date(), updatedModel.date.valueOf(), "Update Date Test"); 183 | 184 | modelResult = ko.viewmodel.toModel(viewmodel); 185 | 186 | deepEqual(modelResult.stringProp, updatedModel.stringProp, "To Model String Test"); 187 | deepEqual(modelResult.id, updatedModel.id, "To Model Number Test"); 188 | deepEqual(modelResult.date, updatedModel.date, "To Model Date Test"); 189 | }); 190 | 191 | test("Exclude", function () { 192 | 193 | var viewmodel = ko.viewmodel.fromModel(model, { 194 | exclude: ["{root}.stringProp"] 195 | }); 196 | 197 | equal(viewmodel.hasOwnProperty("stringProp"), false, "From Model String Prop Not Exist"); 198 | deepEqual(viewmodel.id(), model.id, "From Model Number Test"); 199 | deepEqual(viewmodel.date(), model.date, "From Model Date Test"); 200 | 201 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 202 | 203 | equal(viewmodel.hasOwnProperty("stringProp"), false, "Update... String Prop Not Exist"); 204 | deepEqual(viewmodel.id(), updatedModel.id, "Update Number Test"); 205 | deepEqual(viewmodel.date(), updatedModel.date, "Update Date Test"); 206 | 207 | modelResult = ko.viewmodel.toModel(viewmodel); 208 | 209 | notEqual(modelResult.stringProp, updatedModel.stringProp, "To Model String Test"); 210 | deepEqual(modelResult.id, updatedModel.id, "To Model Number Test"); 211 | deepEqual(modelResult.date, updatedModel.date, "To Model Date Test"); 212 | }); 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /Tests/null-Mapping-qunit-tests.js: -------------------------------------------------------------------------------- 1 | /// 2 | var model, updatedModel, modelResult; 3 | module("Null Mapping Tests", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | 7 | model = { 8 | Prop1: null, 9 | Prop2: "test2", 10 | Prop3: {}, 11 | Prop4: null 12 | }; 13 | 14 | updatedModel = { 15 | Prop1: "test2", 16 | Prop2: null, 17 | Prop3: null, 18 | Prop4: {} 19 | }; 20 | 21 | }, 22 | teardown: function () { 23 | //ko.viewmodel.options.logging = false; 24 | model = undefined; 25 | updatedModel = undefined; 26 | modelResult = undefined; 27 | } 28 | }); 29 | 30 | test("Extend String Null", function () { 31 | 32 | var viewmodel = ko.viewmodel.fromModel(model, { 33 | extend: { 34 | "{root}.Prop1": function (val) { 35 | val.isValid = ko.computed(function () { 36 | return val() != null && val().length > 0; 37 | }); 38 | }, 39 | "{root}.Prop2": function (val) { 40 | val.isValid = ko.computed(function () { 41 | return val() != null && val().length > 0; 42 | }); 43 | } 44 | } 45 | }); 46 | 47 | deepEqual(viewmodel.Prop1(), model.Prop1, "Null Prop Test"); 48 | deepEqual(viewmodel.Prop1.isValid(), false, "Null Prop Extend Test"); 49 | deepEqual(viewmodel.Prop2(), model.Prop2, "String Prop Test"); 50 | deepEqual(viewmodel.Prop2.isValid(), true, "Null Prop Extend Test"); 51 | 52 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 53 | 54 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1, "Null Prop Test"); 55 | deepEqual(viewmodel.Prop1.isValid(), true, "Null Prop Extend Test"); 56 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2, "String Prop Test"); 57 | deepEqual(viewmodel.Prop2.isValid(), false, "Null Prop Extend Test"); 58 | 59 | modelResult = ko.viewmodel.toModel(viewmodel); 60 | 61 | deepEqual(modelResult, updatedModel, "Result Object Comparison"); 62 | }); 63 | 64 | test("Extend Object Null", function () { 65 | 66 | var viewmodel = ko.viewmodel.fromModel(model, { 67 | extend: { 68 | "{root}.Prop3": function (val) { 69 | val.isValid = ko.computed(function () { 70 | return ko.utils.unwrapObservable(val) != null; 71 | }); 72 | }, 73 | "{root}.Prop4": function (val) { 74 | val.isValid = ko.computed(function () { 75 | return ko.utils.unwrapObservable(val) != null; 76 | }); 77 | } 78 | } 79 | }); 80 | 81 | deepEqual(typeof viewmodel.Prop3, "object", "Object Prop Test"); 82 | deepEqual(viewmodel.Prop3.isValid(), true, "Object Prop Extend Test"); 83 | deepEqual(viewmodel.Prop4(), null, "Null Prop Test"); 84 | deepEqual(viewmodel.Prop4.isValid(), false, "Null Prop Extend Test"); 85 | 86 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 87 | 88 | deepEqual(viewmodel.Prop3, null, "Object to Null Prop Update Test"); 89 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4, "Null to Object Update Prop Test"); 90 | deepEqual(viewmodel.Prop4.isValid(), true, "Null to Object Update Prop Extend Test"); 91 | 92 | modelResult = ko.viewmodel.toModel(viewmodel); 93 | 94 | deepEqual(modelResult, updatedModel, "Result Object Comparison"); 95 | }); 96 | 97 | test("Extend Object Null", function () { 98 | 99 | var viewmodel = ko.viewmodel.fromModel(model, { 100 | extend: { 101 | "{root}.Prop1": function (val) { 102 | if (!ko.isObservable(val)) { 103 | val = ko.observable(val) 104 | } 105 | return val; 106 | }, 107 | "{root}.Prop2": function (val) { 108 | if (!ko.isObservable(val)) { 109 | val = ko.observable(val) 110 | } 111 | return val; 112 | }, 113 | "{root}.Prop3": function (val) { 114 | if (!ko.isObservable(val)) { 115 | val = ko.observable(val) 116 | } 117 | return val; 118 | }, 119 | "{root}.Prop4": function (val) { 120 | if (!ko.isObservable(val)) { 121 | val = ko.observable(val) 122 | } 123 | return val; 124 | } 125 | } 126 | }); 127 | 128 | deepEqual(viewmodel.Prop1(), model.Prop1); 129 | deepEqual(viewmodel.Prop2(), model.Prop2); 130 | deepEqual(viewmodel.Prop3(), model.Prop3); 131 | deepEqual(viewmodel.Prop4(), model.Prop4); 132 | 133 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 134 | 135 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1); 136 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2); 137 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3); 138 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4); 139 | 140 | modelResult = ko.viewmodel.toModel(viewmodel); 141 | 142 | deepEqual(updatedModel, modelResult); 143 | }); 144 | 145 | test("Append property", function () { 146 | 147 | var viewmodel = ko.viewmodel.fromModel(model, { 148 | append: ["{root}.Prop1", "{root}.Prop2"] 149 | }); 150 | 151 | deepEqual(viewmodel.Prop1, model.Prop1, "Null to Value Update Test"); 152 | deepEqual(viewmodel.Prop2, model.Prop2, "Value to Null Update Test"); 153 | 154 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 155 | 156 | equal(viewmodel.Prop1, updatedModel.Prop1, "Null to Value Update Test"); 157 | equal(viewmodel.Prop2, updatedModel.Prop2, "Value to Null Update Test"); 158 | 159 | modelResult = ko.viewmodel.toModel(viewmodel); 160 | 161 | equal(modelResult.Prop1, updatedModel.Prop1, "Null to Value Update Test"); 162 | deepEqual(modelResult.Prop2, updatedModel.Prop2, "Value to Null Update Test"); 163 | 164 | }); 165 | 166 | test("Custom basic", function () { 167 | 168 | var viewmodel = ko.viewmodel.fromModel(model, { 169 | custom: { 170 | "{root}.Prop1":function (val) { 171 | return ko.observable(val); 172 | }, 173 | "{root}.Prop2": function (val) { 174 | return ko.observable(val); 175 | }, 176 | "{root}.Prop3": function (val) { 177 | return ko.observable(val); 178 | }, 179 | "{root}.Prop4": function (val) { 180 | return ko.observable(val); 181 | } 182 | } 183 | }); 184 | 185 | deepEqual(viewmodel.Prop1(), model.Prop1); 186 | deepEqual(viewmodel.Prop2(), model.Prop2); 187 | deepEqual(viewmodel.Prop3(), model.Prop3); 188 | deepEqual(viewmodel.Prop4(), model.Prop4); 189 | 190 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 191 | 192 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1); 193 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2); 194 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3); 195 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4); 196 | 197 | modelResult = ko.viewmodel.toModel(viewmodel); 198 | 199 | deepEqual(updatedModel, modelResult); 200 | 201 | }); 202 | 203 | test("Custom map and unmap", function () { 204 | 205 | var viewmodel = ko.viewmodel.fromModel(model, { 206 | custom: { 207 | "{root}.Prop1": { 208 | map: function (val) { 209 | return ko.observable(val); 210 | }, 211 | unmap: function (val) { 212 | return val(); 213 | } 214 | }, 215 | "{root}.Prop2": { 216 | map: function (val) { 217 | return ko.observable(val); 218 | }, 219 | unmap: function (val) { 220 | return val(); 221 | } 222 | }, 223 | "{root}.Prop3": { 224 | map: function (val) { 225 | return ko.observable(val); 226 | }, 227 | unmap: function (val) { 228 | return val(); 229 | } 230 | }, 231 | "{root}.Prop4": { 232 | map: function (val) { 233 | return ko.observable(val); 234 | }, 235 | unmap: function (val) { 236 | return val(); 237 | } 238 | } 239 | } 240 | }); 241 | 242 | deepEqual(viewmodel.Prop1(), model.Prop1); 243 | deepEqual(viewmodel.Prop2(), model.Prop2); 244 | deepEqual(viewmodel.Prop3(), model.Prop3); 245 | deepEqual(viewmodel.Prop4(), model.Prop4); 246 | 247 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 248 | 249 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1); 250 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2); 251 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3); 252 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4); 253 | 254 | modelResult = ko.viewmodel.toModel(viewmodel); 255 | 256 | deepEqual(updatedModel, modelResult); 257 | }); 258 | 259 | test("Exclude", function () { 260 | 261 | var viewmodel = ko.viewmodel.fromModel(model, { 262 | exclude: ["{root}.Prop2"] 263 | }); 264 | 265 | equal(viewmodel.hasOwnProperty("Prop2"), false, "From Model String Prop Not Exist"); 266 | 267 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 268 | 269 | equal(viewmodel.hasOwnProperty("Prop2"), false, "Update... String Prop Not Exist"); 270 | 271 | modelResult = ko.viewmodel.toModel(viewmodel); 272 | 273 | equal(modelResult.hasOwnProperty("Prop2"), false, "Update... String Prop Not Exist"); 274 | }); 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /Tests/undefined-Mapping-qunit-tests.js: -------------------------------------------------------------------------------- 1 | /// 2 | var model, updatedModel, modelResult; 3 | module("Undefined Mapping Tests", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | 7 | model = { 8 | Prop1: undefined, 9 | Prop2: "test2", 10 | Prop3: {}, 11 | Prop4: undefined 12 | }; 13 | 14 | updatedModel = { 15 | Prop1: "test2", 16 | Prop2: undefined, 17 | Prop3: undefined, 18 | Prop4: {} 19 | }; 20 | 21 | }, 22 | teardown: function () { 23 | //ko.viewmodel.options.logging = false; 24 | model = undefined; 25 | updatedModel = undefined; 26 | modelResult = undefined; 27 | } 28 | }); 29 | 30 | test("Extend String Undefined", function () { 31 | 32 | var viewmodel = ko.viewmodel.fromModel(model, { 33 | extend: { 34 | "{root}.Prop1": function (val) { 35 | val.isValid = ko.computed(function () { 36 | return val() != undefined && val().length > 0; 37 | }); 38 | }, 39 | "{root}.Prop2": function (val) { 40 | val.isValid = ko.computed(function () { 41 | return val() != undefined && val().length > 0; 42 | }); 43 | } 44 | } 45 | }); 46 | 47 | deepEqual(viewmodel.Prop1(), model.Prop1, "Undefined Prop Test"); 48 | deepEqual(viewmodel.Prop1.isValid(), false, "Undefined Prop Extend Test"); 49 | deepEqual(viewmodel.Prop2(), model.Prop2, "String Prop Test"); 50 | deepEqual(viewmodel.Prop2.isValid(), true, "Undefined Prop Extend Test"); 51 | 52 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 53 | 54 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1, "Undefined Prop Test"); 55 | deepEqual(viewmodel.Prop1.isValid(), true, "Undefined Prop Extend Test"); 56 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2, "String Prop Test"); 57 | deepEqual(viewmodel.Prop2.isValid(), false, "Undefined Prop Extend Test"); 58 | 59 | modelResult = ko.viewmodel.toModel(viewmodel); 60 | 61 | deepEqual(modelResult, updatedModel, "Result Object Comparison"); 62 | }); 63 | 64 | test("Extend Object Undefined", function () { 65 | 66 | var viewmodel = ko.viewmodel.fromModel(model, { 67 | extend: { 68 | "{root}.Prop3": function (val) { 69 | val.isValid = ko.computed(function () { 70 | return ko.utils.unwrapObservable(val) != undefined; 71 | }); 72 | }, 73 | "{root}.Prop4": function (val) { 74 | val.isValid = ko.computed(function () { 75 | return ko.utils.unwrapObservable(val) != undefined; 76 | }); 77 | } 78 | } 79 | }); 80 | 81 | deepEqual(typeof viewmodel.Prop3, "object", "Object Prop Test"); 82 | deepEqual(viewmodel.Prop3.isValid(), true, "Object Prop Extend Test"); 83 | deepEqual(viewmodel.Prop4(), undefined, "Undefined Prop Test"); 84 | deepEqual(viewmodel.Prop4.isValid(), false, "Undefined Prop Extend Test"); 85 | 86 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 87 | 88 | deepEqual(viewmodel.Prop3, undefined, "Object to Undefined Prop Update Test"); 89 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4, "Undefined to Object Update Prop Test"); 90 | deepEqual(viewmodel.Prop4.isValid(), true, "Undefined to Object Update Prop Extend Test"); 91 | 92 | modelResult = ko.viewmodel.toModel(viewmodel); 93 | 94 | deepEqual(modelResult, updatedModel, "Result Object Comparison"); 95 | }); 96 | 97 | test("Extend Object Undefined", function () { 98 | 99 | var viewmodel = ko.viewmodel.fromModel(model, { 100 | extend: { 101 | "{root}.Prop1": function (val) { 102 | if (!ko.isObservable(val)) { 103 | val = ko.observable(val) 104 | } 105 | return val; 106 | }, 107 | "{root}.Prop2": function (val) { 108 | if (!ko.isObservable(val)) { 109 | val = ko.observable(val) 110 | } 111 | return val; 112 | }, 113 | "{root}.Prop3": function (val) { 114 | if (!ko.isObservable(val)) { 115 | val = ko.observable(val) 116 | } 117 | return val; 118 | }, 119 | "{root}.Prop4": function (val) { 120 | if (!ko.isObservable(val)) { 121 | val = ko.observable(val) 122 | } 123 | return val; 124 | } 125 | } 126 | }); 127 | 128 | deepEqual(viewmodel.Prop1(), model.Prop1); 129 | deepEqual(viewmodel.Prop2(), model.Prop2); 130 | deepEqual(viewmodel.Prop3(), model.Prop3); 131 | deepEqual(viewmodel.Prop4(), model.Prop4); 132 | 133 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 134 | 135 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1); 136 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2); 137 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3); 138 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4); 139 | 140 | modelResult = ko.viewmodel.toModel(viewmodel); 141 | 142 | deepEqual(updatedModel, modelResult); 143 | }); 144 | 145 | test("Append property", function () { 146 | 147 | var viewmodel = ko.viewmodel.fromModel(model, { 148 | append: ["{root}.Prop1", "{root}.Prop2"] 149 | }); 150 | 151 | deepEqual(viewmodel.Prop1, model.Prop1, "Undefined to Value Update Test"); 152 | deepEqual(viewmodel.Prop2, model.Prop2, "Value to Undefined Update Test"); 153 | 154 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 155 | 156 | deepEqual(viewmodel.Prop1, updatedModel.Prop1, "Undefined to Value Update Test"); 157 | deepEqual(viewmodel.Prop2, updatedModel.Prop2, "Value to Undefined Update Test"); 158 | 159 | modelResult = ko.viewmodel.toModel(viewmodel); 160 | 161 | deepEqual(modelResult.Prop1, updatedModel.Prop1, "Undefined to Value Update Test"); 162 | deepEqual(modelResult.Prop2, updatedModel.Prop2, "Value to Undefined Update Test"); 163 | 164 | }); 165 | 166 | test("Custom basic", function () { 167 | 168 | var viewmodel = ko.viewmodel.fromModel(model, { 169 | custom: { 170 | "{root}.Prop1":function (val) { 171 | return ko.observable(val); 172 | }, 173 | "{root}.Prop2": function (val) { 174 | return ko.observable(val); 175 | }, 176 | "{root}.Prop3": function (val) { 177 | return ko.observable(val); 178 | }, 179 | "{root}.Prop4": function (val) { 180 | return ko.observable(val); 181 | } 182 | } 183 | }); 184 | 185 | deepEqual(viewmodel.Prop1(), model.Prop1); 186 | deepEqual(viewmodel.Prop2(), model.Prop2); 187 | deepEqual(viewmodel.Prop3(), model.Prop3); 188 | deepEqual(viewmodel.Prop4(), model.Prop4); 189 | 190 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 191 | 192 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1); 193 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2); 194 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3); 195 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4); 196 | 197 | modelResult = ko.viewmodel.toModel(viewmodel); 198 | 199 | deepEqual(updatedModel, modelResult); 200 | 201 | }); 202 | 203 | test("Custom map and unmap", function () { 204 | 205 | var viewmodel = ko.viewmodel.fromModel(model, { 206 | custom: { 207 | "{root}.Prop1": { 208 | map: function (val) { 209 | return ko.observable(val); 210 | }, 211 | unmap: function (val) { 212 | return val(); 213 | } 214 | }, 215 | "{root}.Prop2": { 216 | map: function (val) { 217 | return ko.observable(val); 218 | }, 219 | unmap: function (val) { 220 | return val(); 221 | } 222 | }, 223 | "{root}.Prop3": { 224 | map: function (val) { 225 | return ko.observable(val); 226 | }, 227 | unmap: function (val) { 228 | return val(); 229 | } 230 | }, 231 | "{root}.Prop4": { 232 | map: function (val) { 233 | return ko.observable(val); 234 | }, 235 | unmap: function (val) { 236 | return val(); 237 | } 238 | } 239 | } 240 | }); 241 | 242 | deepEqual(viewmodel.Prop1(), model.Prop1); 243 | deepEqual(viewmodel.Prop2(), model.Prop2); 244 | deepEqual(viewmodel.Prop3(), model.Prop3); 245 | deepEqual(viewmodel.Prop4(), model.Prop4); 246 | 247 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 248 | 249 | deepEqual(viewmodel.Prop1(), updatedModel.Prop1); 250 | deepEqual(viewmodel.Prop2(), updatedModel.Prop2); 251 | deepEqual(viewmodel.Prop3(), updatedModel.Prop3); 252 | deepEqual(viewmodel.Prop4(), updatedModel.Prop4); 253 | 254 | modelResult = ko.viewmodel.toModel(viewmodel); 255 | 256 | deepEqual(updatedModel, modelResult); 257 | }); 258 | 259 | test("Exclude", function () { 260 | 261 | var viewmodel = ko.viewmodel.fromModel(model, { 262 | exclude: ["{root}.Prop2"] 263 | }); 264 | 265 | equal(viewmodel.hasOwnProperty("Prop2"), false, "From Model String Prop Not Exist"); 266 | 267 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 268 | 269 | equal(viewmodel.hasOwnProperty("Prop2"), false, "Update... String Prop Not Exist"); 270 | 271 | modelResult = ko.viewmodel.toModel(viewmodel); 272 | 273 | equal(modelResult.hasOwnProperty("Prop2"), false, "Update... String Prop Not Exist"); 274 | }); 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /web/stylesheets/stylesheet.css: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Slate Theme for Github Pages 3 | by Jason Costello, @jsncostello 4 | *******************************************************************************/ 5 | 6 | @import url(pygment_trac.css); 7 | 8 | /******************************************************************************* 9 | MeyerWeb Reset 10 | *******************************************************************************/ 11 | 12 | html, body, div, span, applet, object, iframe, 13 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 14 | a, abbr, acronym, address, big, cite, code, 15 | del, dfn, em, img, ins, kbd, q, s, samp, 16 | small, strike, strong, sub, sup, tt, var, 17 | b, u, i, center, 18 | dl, dt, dd, ol, ul, li, 19 | fieldset, form, label, legend, 20 | table, caption, tbody, tfoot, thead, tr, th, td, 21 | article, aside, canvas, details, embed, 22 | figure, figcaption, footer, header, hgroup, 23 | menu, nav, output, ruby, section, summary, 24 | time, mark, audio, video { 25 | margin: 0; 26 | padding: 0; 27 | border: 0; 28 | font: inherit; 29 | vertical-align: baseline; 30 | } 31 | 32 | /* HTML5 display-role reset for older browsers */ 33 | article, aside, details, figcaption, figure, 34 | footer, header, hgroup, menu, nav, section { 35 | display: block; 36 | } 37 | 38 | ol, ul { 39 | list-style: none; 40 | } 41 | 42 | blockquote, q { 43 | } 44 | 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | a:focus { 51 | outline: none; 52 | } 53 | 54 | /******************************************************************************* 55 | Theme Styles 56 | *******************************************************************************/ 57 | 58 | body { 59 | box-sizing: border-box; 60 | color:#373737; 61 | background: #f2f2f2; 62 | font-size: 16px; 63 | font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif; 64 | line-height: 1.5; 65 | -webkit-font-smoothing: antialiased; 66 | height:100%; 67 | } 68 | 69 | h1, h2, h3, h4, h5, h6 { 70 | margin: 10px 0; 71 | font-weight: 700; 72 | color:#222222; 73 | font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; 74 | letter-spacing: -1px; 75 | } 76 | 77 | h1 { 78 | font-size: 36px; 79 | font-weight: 700; 80 | } 81 | 82 | h2 { 83 | padding-bottom: 10px; 84 | font-size: 32px; 85 | background: url('../images/bg_hr.png') repeat-x bottom; 86 | } 87 | 88 | h3 { 89 | font-size: 24px; 90 | } 91 | 92 | h4 { 93 | font-size: 21px; 94 | } 95 | 96 | h5 { 97 | font-size: 18px; 98 | } 99 | 100 | h6 { 101 | font-size: 16px; 102 | } 103 | 104 | p { 105 | margin: 10px 0 15px 0; 106 | } 107 | 108 | footer p { 109 | color: #f2f2f2; 110 | } 111 | 112 | a { 113 | text-decoration: none; 114 | color: #007edf; 115 | text-shadow: none; 116 | 117 | transition: color 0.5s ease; 118 | transition: text-shadow 0.5s ease; 119 | -webkit-transition: color 0.5s ease; 120 | -webkit-transition: text-shadow 0.5s ease; 121 | -moz-transition: color 0.5s ease; 122 | -moz-transition: text-shadow 0.5s ease; 123 | -o-transition: color 0.5s ease; 124 | -o-transition: text-shadow 0.5s ease; 125 | -ms-transition: color 0.5s ease; 126 | -ms-transition: text-shadow 0.5s ease; 127 | } 128 | 129 | #main_content a:hover { 130 | color: #0069ba; 131 | text-shadow: #0090ff 0px 0px 2px; 132 | } 133 | 134 | footer a:hover { 135 | color: #43adff; 136 | text-shadow: #0090ff 0px 0px 2px; 137 | } 138 | 139 | em { 140 | font-style: italic; 141 | } 142 | 143 | strong { 144 | font-weight: bold; 145 | } 146 | 147 | img { 148 | position: relative; 149 | margin: 0 auto; 150 | max-width: 739px; 151 | padding: 5px; 152 | margin: 10px 0 10px 0; 153 | border: 1px solid #ebebeb; 154 | 155 | box-shadow: 0 0 5px #ebebeb; 156 | -webkit-box-shadow: 0 0 5px #ebebeb; 157 | -moz-box-shadow: 0 0 5px #ebebeb; 158 | -o-box-shadow: 0 0 5px #ebebeb; 159 | -ms-box-shadow: 0 0 5px #ebebeb; 160 | } 161 | 162 | pre, code { 163 | width: 100%; 164 | color: #222; 165 | background-color: #fff; 166 | 167 | font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; 168 | font-size: 14px; 169 | 170 | border-radius: 2px; 171 | -moz-border-radius: 2px; 172 | -webkit-border-radius: 2px; 173 | 174 | 175 | 176 | } 177 | 178 | pre { 179 | width: 100%; 180 | padding: 10px; 181 | box-shadow: 0 0 10px rgba(0,0,0,.1); 182 | overflow: auto; 183 | } 184 | 185 | code { 186 | padding: 3px; 187 | margin: 0 3px; 188 | box-shadow: 0 0 10px rgba(0,0,0,.1); 189 | } 190 | 191 | pre code { 192 | display: block; 193 | box-shadow: none; 194 | } 195 | 196 | blockquote { 197 | color: #666; 198 | margin-bottom: 20px; 199 | padding: 0 0 0 20px; 200 | border-left: 3px solid #bbb; 201 | } 202 | 203 | ul, ol, dl { 204 | margin-bottom: 15px 205 | } 206 | 207 | ul li { 208 | list-style: inside; 209 | padding-left: 20px; 210 | } 211 | 212 | ol li { 213 | list-style: decimal inside; 214 | padding-left: 20px; 215 | } 216 | 217 | dl dt { 218 | font-weight: bold; 219 | } 220 | 221 | dl dd { 222 | padding-left: 20px; 223 | font-style: italic; 224 | } 225 | 226 | dl p { 227 | padding-left: 20px; 228 | font-style: italic; 229 | } 230 | 231 | hr { 232 | height: 1px; 233 | margin-bottom: 5px; 234 | border: none; 235 | background: url('../images/bg_hr.png') repeat-x center; 236 | } 237 | 238 | table { 239 | border: 1px solid #373737; 240 | margin-bottom: 20px; 241 | text-align: left; 242 | } 243 | 244 | th { 245 | font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif; 246 | padding: 10px; 247 | background: #373737; 248 | color: #fff; 249 | } 250 | 251 | td { 252 | padding: 10px; 253 | border: 1px solid #373737; 254 | } 255 | 256 | form { 257 | background: #f2f2f2; 258 | padding: 20px; 259 | } 260 | 261 | img { 262 | width: 100%; 263 | max-width: 100%; 264 | } 265 | 266 | /******************************************************************************* 267 | Full-Width Styles 268 | *******************************************************************************/ 269 | 270 | .outer { 271 | width: 100%; 272 | } 273 | 274 | .inner { 275 | position: relative; 276 | max-width: 900px; 277 | padding: 5px 10px 5px 10px; 278 | margin: 0 auto; 279 | } 280 | 281 | .inner .inner { 282 | margin-left:15px; 283 | } 284 | 285 | #forkme_banner { 286 | display: block; 287 | position: absolute; 288 | top:0; 289 | right: 10px; 290 | z-index: 10; 291 | padding: 10px 50px 10px 10px; 292 | color: #fff; 293 | background: url('../images/blacktocat.png') #333 no-repeat 95% 50%; 294 | font-weight: 700; 295 | box-shadow: 0 0 10px rgba(0,0,0,.5); 296 | border-bottom-left-radius: 2px; 297 | border-bottom-right-radius: 2px; 298 | } 299 | 300 | #forkme_banner:hover { 301 | background-color: orange; 302 | } 303 | 304 | #header_wrap { 305 | background: #212121; 306 | background: -moz-linear-gradient(top, #B85C1E, Orange); 307 | background: -webkit-linear-gradient(top, #B85C1E, Orange); 308 | background: -ms-linear-gradient(top, #B85C1E, Orange); 309 | background: -o-linear-gradient(top, #B85C1E, Orange); 310 | background: linear-gradient(top, #B85C1E, Orange); 311 | } 312 | 313 | #header_wrap .inner { 314 | padding: 50px 10px 30px 10px; 315 | } 316 | 317 | #project_title { 318 | margin: 0; 319 | color: #fff; 320 | font-size: 42px; 321 | font-weight: 700; 322 | text-shadow: #111 0px 0px 10px; 323 | } 324 | 325 | #project_tagline { 326 | color: #fff; 327 | font-size: 24px; 328 | font-weight: 300; 329 | background: none; 330 | text-shadow: #111 0px 0px 10px; 331 | } 332 | 333 | #downloads { 334 | position: absolute; 335 | width: 210px; 336 | z-index: 10; 337 | bottom: -40px; 338 | right: 0; 339 | height: 70px; 340 | background: url('../images/icon_download.png') no-repeat 0% 90%; 341 | } 342 | 343 | .zip_download_link { 344 | display: block; 345 | float: right; 346 | width: 90px; 347 | height:70px; 348 | text-indent: -5000px; 349 | overflow: hidden; 350 | background: url(../images/sprite_download.png) no-repeat bottom left; 351 | } 352 | 353 | .tar_download_link { 354 | display: block; 355 | float: right; 356 | width: 90px; 357 | height:70px; 358 | text-indent: -5000px; 359 | overflow: hidden; 360 | background: url(../images/sprite_download.png) no-repeat bottom right; 361 | margin-left: 10px; 362 | } 363 | 364 | .zip_download_link:hover { 365 | background: url(../images/sprite_download.png) no-repeat top left; 366 | } 367 | 368 | .tar_download_link:hover { 369 | background: url(../images/sprite_download.png) no-repeat top right; 370 | } 371 | 372 | #main_content_wrap { 373 | background: #f2f2f2; 374 | border-top: 1px solid #111; 375 | } 376 | 377 | #main_content { 378 | padding-top: 40px; 379 | } 380 | 381 | #footer_wrap { 382 | background: #212121; 383 | min-height:130px; 384 | } 385 | 386 | #qunit { 387 | margin-top:30px; 388 | margin-bottom:50px; 389 | } 390 | 391 | 392 | 393 | /******************************************************************************* 394 | Small Device Styles 395 | *******************************************************************************/ 396 | 397 | @media screen and (max-width: 480px) { 398 | body { 399 | font-size:14px; 400 | } 401 | 402 | #downloads { 403 | display: none; 404 | } 405 | 406 | .inner { 407 | min-width: 320px; 408 | max-width: 480px; 409 | } 410 | 411 | #project_title { 412 | font-size: 32px; 413 | } 414 | 415 | h1 { 416 | font-size: 28px; 417 | } 418 | 419 | h2 { 420 | font-size: 24px; 421 | } 422 | 423 | h3 { 424 | font-size: 21px; 425 | } 426 | 427 | h4 { 428 | font-size: 18px; 429 | } 430 | 431 | h5 { 432 | font-size: 14px; 433 | } 434 | 435 | h6 { 436 | font-size: 12px; 437 | } 438 | 439 | code, pre { 440 | min-width: 320px; 441 | max-width: 480px; 442 | font-size: 11px; 443 | } 444 | 445 | } 446 | -------------------------------------------------------------------------------- /Tests/nestedObject-qunit-tests.js: -------------------------------------------------------------------------------- 1 | /// 2 | var model, updatedModel, modelResult; 3 | module("Nested Object Types", { 4 | setup: function () { 5 | //ko.viewmodel.options.logging = true; 6 | 7 | model = { 8 | data: { 9 | stringProp: "test", 10 | id: 5, 11 | date: new Date("01/01/2001") 12 | } 13 | }; 14 | 15 | updatedModel = { 16 | data: { 17 | stringProp: "test2", 18 | id: 6, 19 | date: new Date("02/01/2002") 20 | } 21 | }; 22 | 23 | }, 24 | teardown: function () { 25 | //ko.viewmodel.options.logging = false; 26 | model = undefined; 27 | updatedModel = undefined; 28 | modelResult = undefined; 29 | } 30 | }); 31 | 32 | 33 | test("Basic", function () { 34 | 35 | var viewmodel = ko.viewmodel.fromModel(model); 36 | 37 | deepEqual(viewmodel.data.stringProp(), model.data.stringProp, "From Model String Test"); 38 | deepEqual(viewmodel.data.id(), model.data.id, "From Model Number Test"); 39 | deepEqual(viewmodel.data.date(), model.data.date, "From Model Date Test"); 40 | 41 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 42 | 43 | deepEqual(viewmodel.data.stringProp(), updatedModel.data.stringProp, "Update String Test"); 44 | deepEqual(viewmodel.data.id(), updatedModel.data.id, "Update Number Test"); 45 | deepEqual(viewmodel.data.date(), updatedModel.data.date, "Update Date Test"); 46 | 47 | modelResult = ko.viewmodel.toModel(viewmodel); 48 | 49 | deepEqual(modelResult, updatedModel, "Result Object Comparison"); 50 | }); 51 | 52 | test("Extend", function () { 53 | 54 | var viewmodel = ko.viewmodel.fromModel(model, { 55 | extend: { 56 | "{root}": function(root){ 57 | root.isValid = ko.computed(function () { 58 | return root.data.stringProp.isValid() && root.data.id.isValid() && root.data.date.isValid(); 59 | }); 60 | }, 61 | "{root}.data.stringProp": function (stringProp) { 62 | stringProp.isValid = ko.computed(function () { 63 | return stringProp() && stringProp().length; 64 | }); 65 | }, 66 | "{root}.data.id": function (id) { 67 | id.isValid = ko.computed(function () { 68 | return id() && id() > 0; 69 | }); 70 | }, 71 | "{root}.data.date": function (date) { 72 | date.isValid = ko.computed(function () { 73 | return date() && date() < new Date(); 74 | }); 75 | } 76 | } 77 | }); 78 | 79 | deepEqual(viewmodel.data.stringProp(), model.data.stringProp, "From Model String Test"); 80 | deepEqual(viewmodel.data.id(), model.data.id, "From Model Number Test"); 81 | deepEqual(viewmodel.data.date(), model.data.date, "From Model Date Test"); 82 | deepEqual(viewmodel.isValid(), true, "Extension check"); 83 | 84 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 85 | 86 | deepEqual(viewmodel.data.stringProp(), updatedModel.data.stringProp, "Update String Test"); 87 | deepEqual(viewmodel.data.id(), updatedModel.data.id, "Update Number Test"); 88 | deepEqual(viewmodel.data.date(), updatedModel.data.date, "Update Date Test"); 89 | deepEqual(viewmodel.isValid(), true, "Extension check"); 90 | 91 | modelResult = ko.viewmodel.toModel(viewmodel); 92 | 93 | deepEqual(viewmodel.isValid(), true, "Extension check"); 94 | }); 95 | 96 | test("Append object", function () { 97 | 98 | var viewmodel = ko.viewmodel.fromModel(model, { 99 | append: ["{root}.data"] 100 | }); 101 | 102 | deepEqual(viewmodel, model, "From Model Test"); 103 | 104 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 105 | 106 | notEqual(viewmodel, updatedModel, "Update Fail Test"); 107 | 108 | modelResult = ko.viewmodel.toModel(viewmodel); 109 | 110 | deepEqual(modelResult, model, "Result"); 111 | }); 112 | 113 | test("Append object property", function () { 114 | 115 | var viewmodel = ko.viewmodel.fromModel(model, { 116 | append: ["{root}.data.stringProp"] 117 | }); 118 | 119 | deepEqual(viewmodel.data.stringProp, model.data.stringProp, "From Model String Test"); 120 | deepEqual(viewmodel.data.id(), model.data.id, "From Model Number Test"); 121 | deepEqual(viewmodel.data.date(), model.data.date, "From Model Date Test"); 122 | 123 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 124 | 125 | deepEqual(viewmodel.data.stringProp, updatedModel.data.stringProp, "From Model String Test Fail"); 126 | deepEqual(viewmodel.data.id(), updatedModel.data.id, "Update Number Test"); 127 | deepEqual(viewmodel.data.date(), updatedModel.data.date, "Update Date Test"); 128 | 129 | modelResult = ko.viewmodel.toModel(viewmodel); 130 | 131 | deepEqual(modelResult.data.stringProp, updatedModel.data.stringProp, "To Model String Test"); 132 | deepEqual(modelResult.data.id, updatedModel.data.id, "To Model Number Test"); 133 | deepEqual(modelResult.data.date, updatedModel.data.date, "To Model Date Test"); 134 | }); 135 | 136 | test("Custom basic", function () { 137 | 138 | var viewmodel = ko.viewmodel.fromModel(model, { 139 | custom: { 140 | "{root}.data.date": function (date) { 141 | return date.valueOf(); 142 | } 143 | } 144 | }); 145 | 146 | deepEqual(viewmodel.data.stringProp(), model.data.stringProp, "From Model String Test"); 147 | deepEqual(viewmodel.data.id(), model.data.id, "From Model Number Test"); 148 | deepEqual(viewmodel.data.date, model.data.date.valueOf(), "From Model Date Test"); 149 | 150 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 151 | 152 | deepEqual(viewmodel.data.stringProp(), updatedModel.data.stringProp, "Update String Test"); 153 | deepEqual(viewmodel.data.id(), updatedModel.data.id, "Update Number Test"); 154 | deepEqual(viewmodel.data.date, updatedModel.data.date.valueOf(), "Update Date Test"); 155 | 156 | modelResult = ko.viewmodel.toModel(viewmodel); 157 | 158 | deepEqual(modelResult.data.stringProp, updatedModel.data.stringProp, "To Model String Test"); 159 | deepEqual(modelResult.data.id, updatedModel.data.id, "To Model Number Test"); 160 | deepEqual(modelResult.data.date, updatedModel.data.date.valueOf(), "To Model Date Test"); 161 | }); 162 | 163 | test("Custom basic", function () { 164 | 165 | var viewmodel = ko.viewmodel.fromModel(model, { 166 | custom: { 167 | "{root}.data.date": function (date) { 168 | return date.valueOf(); 169 | } 170 | } 171 | }); 172 | 173 | deepEqual(viewmodel.data.stringProp(), model.data.stringProp, "From Model String Test"); 174 | deepEqual(viewmodel.data.id(), model.data.id, "From Model Number Test"); 175 | deepEqual(viewmodel.data.date, model.data.date.valueOf(), "From Model Date Test"); 176 | 177 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 178 | 179 | deepEqual(viewmodel.data.stringProp(), updatedModel.data.stringProp, "Update String Test"); 180 | deepEqual(viewmodel.data.id(), updatedModel.data.id, "Update Number Test"); 181 | deepEqual(viewmodel.data.date, updatedModel.data.date.valueOf(), "Update Date Test"); 182 | 183 | modelResult = ko.viewmodel.toModel(viewmodel); 184 | 185 | deepEqual(modelResult.data.stringProp, updatedModel.data.stringProp, "To Model String Test"); 186 | deepEqual(modelResult.data.id, updatedModel.data.id, "To Model Number Test"); 187 | deepEqual(modelResult.data.date, updatedModel.data.date.valueOf(), "To Model Date Test"); 188 | }); 189 | 190 | test("Custom map and unmap", function () { 191 | 192 | var viewmodel = ko.viewmodel.fromModel(model, { 193 | custom: { 194 | "{root}.data.date": { 195 | map: function (date) { 196 | return ko.observable(date.valueOf()); 197 | }, 198 | unmap: function(date){ 199 | return new Date(date()); 200 | } 201 | } 202 | } 203 | }); 204 | 205 | deepEqual(viewmodel.data.stringProp(), model.data.stringProp, "From Model String Test"); 206 | deepEqual(viewmodel.data.id(), model.data.id, "From Model Number Test"); 207 | deepEqual(viewmodel.data.date(), model.data.date.valueOf(), "From Model Date Test"); 208 | 209 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 210 | 211 | deepEqual(viewmodel.data.stringProp(), updatedModel.data.stringProp, "Update String Test"); 212 | deepEqual(viewmodel.data.id(), updatedModel.data.id, "Update Number Test"); 213 | deepEqual(viewmodel.data.date(), updatedModel.data.date.valueOf(), "Update Date Test"); 214 | 215 | modelResult = ko.viewmodel.toModel(viewmodel); 216 | 217 | deepEqual(modelResult.data.stringProp, updatedModel.data.stringProp, "To Model String Test"); 218 | deepEqual(modelResult.data.id, updatedModel.data.id, "To Model Number Test"); 219 | deepEqual(modelResult.data.date, updatedModel.data.date, "To Model Date Test"); 220 | }); 221 | 222 | test("Exclude", function () { 223 | 224 | var viewmodel = ko.viewmodel.fromModel(model, { 225 | exclude: ["{root}.data.stringProp"] 226 | }); 227 | 228 | equal(viewmodel.data.hasOwnProperty("stringProp"), false, "From Model String Prop Not Exist"); 229 | deepEqual(viewmodel.data.id(), model.data.id, "From Model Number Test"); 230 | deepEqual(viewmodel.data.date(), model.data.date, "From Model Date Test"); 231 | 232 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 233 | 234 | equal(viewmodel.data.hasOwnProperty("stringProp"), false, "Update... String Prop Not Exist"); 235 | deepEqual(viewmodel.data.id(), updatedModel.data.id, "Update Number Test"); 236 | deepEqual(viewmodel.data.date(), updatedModel.data.date, "Update Date Test"); 237 | 238 | modelResult = ko.viewmodel.toModel(viewmodel); 239 | 240 | notEqual(modelResult.data.stringProp, updatedModel.data.stringProp, "To Model String Test"); 241 | deepEqual(modelResult.data.id, updatedModel.data.id, "To Model Number Test"); 242 | deepEqual(modelResult.data.date, updatedModel.data.date, "To Model Date Test"); 243 | }); 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /Tests/updateFromModel-Basic-qunit-tests.js: -------------------------------------------------------------------------------- 1 | module("updateFromModel Basic", { 2 | setup: function () { 3 | //ko.viewmodel.options.logging = true; 4 | }, 5 | teardown: function () { 6 | //ko.viewmodel.options.logging = false; 7 | } 8 | }); 9 | 10 | 11 | test("Default simple types", function () { 12 | var model, updatedModel, viewmodel; 13 | 14 | model = { 15 | stringProp: "test", 16 | number: 5, 17 | date: new Date("01/01/2001") 18 | }; 19 | 20 | updatedModel = { 21 | stringProp: "test2", 22 | number: 6, 23 | date: new Date("12/04/2001") 24 | }; 25 | 26 | viewmodel = ko.viewmodel.fromModel(model); 27 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 28 | 29 | deepEqual(viewmodel.stringProp(), updatedModel.stringProp, "String Test"); 30 | deepEqual(viewmodel.number(), updatedModel.number, "Number Test"); 31 | deepEqual(viewmodel.date(), updatedModel.date, "Date Test"); 32 | }); 33 | 34 | test("nested object simple types", function () { 35 | var model, updatedModel, viewmodel; 36 | 37 | model = { 38 | test:{ 39 | stringProp: "test", 40 | number: 5, 41 | date: new Date("01/01/2001") 42 | } 43 | }; 44 | 45 | updatedModel = { 46 | test: { 47 | stringProp: "test2", 48 | number: 6, 49 | date: new Date("12/04/2001") 50 | } 51 | }; 52 | 53 | viewmodel = ko.viewmodel.fromModel(model); 54 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 55 | 56 | deepEqual(viewmodel.test.stringProp(), updatedModel.test.stringProp, "String Test"); 57 | deepEqual(viewmodel.test.number(), updatedModel.test.number, "Number Test"); 58 | deepEqual(viewmodel.test.date(), updatedModel.test.date, "Date Test"); 59 | }); 60 | 61 | test("nested object override success simple types", function () { 62 | var model, updatedModel, viewmodel, options; 63 | 64 | options = { 65 | override:["{root}.test"] 66 | }; 67 | 68 | model = { 69 | test: { 70 | stringProp: "test", 71 | number: 5, 72 | date: new Date("01/01/2001") 73 | } 74 | }; 75 | 76 | updatedModel = { 77 | test: { 78 | stringProp: "test2", 79 | number: 6, 80 | date: new Date("12/04/2001") 81 | } 82 | }; 83 | 84 | viewmodel = ko.viewmodel.fromModel(model, options); 85 | deepEqual(viewmodel.test.stringProp(), model.test.stringProp, "Viewmodel String Test"); 86 | deepEqual(viewmodel.test.number(), model.test.number, "Viewmodel Number Test"); 87 | deepEqual(viewmodel.test.date(), model.test.date, "Viewmodel Date Test"); 88 | 89 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 90 | 91 | deepEqual(viewmodel.test.stringProp(), updatedModel.test.stringProp, "UpdatedModel String Test"); 92 | deepEqual(viewmodel.test.number(), updatedModel.test.number, "UpdatedModel Number Test"); 93 | deepEqual(viewmodel.test.date(), updatedModel.test.date, "UpdatedModel Date Test"); 94 | }); 95 | 96 | //if undefined then we are using facade around ko.mapping 97 | //used to exclude tests that are incompatable with ko.mapping 98 | if (ko.viewmodel.options.mappingCompatability !== undefined) { 99 | test("ID arrayChildId match array object simple types", function () { 100 | var model, updatedModel, viewmodel, options, originalArrayItem; 101 | 102 | model = { 103 | items: [ 104 | { 105 | id: 5, 106 | stringProp: "test", 107 | number: 3, 108 | date: new Date("2/04/2001") 109 | } 110 | ] 111 | }; 112 | 113 | updatedModel = { 114 | items: [ 115 | { 116 | id: 5, 117 | stringProp: "test2", 118 | number: 6, 119 | date: new Date("12/04/2001") 120 | }, 121 | { 122 | id: 6, 123 | stringProp: "test", 124 | number: 3, 125 | date: new Date("2/04/2001") 126 | } 127 | ] 128 | }; 129 | 130 | options = { 131 | arrayChildId: { 132 | "{root}.items": "id" 133 | } 134 | }; 135 | 136 | viewmodel = ko.viewmodel.fromModel(model, options); 137 | originalArrayItem = viewmodel.items()[0]; 138 | deepEqual(originalArrayItem.id(), 5, "verify id before update"); 139 | 140 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 141 | 142 | deepEqual(viewmodel.items()[0].id(), 5, "verify id after update"); 143 | deepEqual(viewmodel.items()[0] === originalArrayItem, true, "verify still same object"); 144 | deepEqual(viewmodel.items()[0].id(), updatedModel.items[0].id, "String Test"); 145 | deepEqual(viewmodel.items()[0].stringProp(), updatedModel.items[0].stringProp, "String Test"); 146 | deepEqual(viewmodel.items()[0].number(), updatedModel.items[0].number, "String Test"); 147 | deepEqual(viewmodel.items()[0].date(), updatedModel.items[0].date, "String Test"); 148 | }); 149 | } 150 | 151 | test("No arrayChildId option array object simple types", function () { 152 | var model, updatedModel, viewmodel, options, originalArrayItem; 153 | 154 | model = { 155 | items: [ 156 | { 157 | id: 5, 158 | stringProp: "test", 159 | number: 3, 160 | date: new Date("2/04/2001") 161 | } 162 | ] 163 | }; 164 | 165 | updatedModel = { 166 | items: [ 167 | { 168 | id: 5, 169 | stringProp: "test2", 170 | number: 6, 171 | date: new Date("12/04/2001") 172 | }, 173 | { 174 | id: 6, 175 | stringProp: "test", 176 | number: 3, 177 | date: new Date("2/04/2001") 178 | } 179 | ] 180 | }; 181 | 182 | options = {}; 183 | 184 | viewmodel = ko.viewmodel.fromModel(model, options); 185 | originalArrayItem = viewmodel.items()[0]; 186 | deepEqual(originalArrayItem.id(), 5, "verify id before update"); 187 | 188 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 189 | 190 | deepEqual(viewmodel.items()[0].id(), 5, "verify id after update"); 191 | deepEqual(viewmodel.items()[0] !== originalArrayItem, true, "verify not still same object"); 192 | deepEqual(viewmodel.items()[0].id(), updatedModel.items[0].id, "String Test"); 193 | deepEqual(viewmodel.items()[0].stringProp(), updatedModel.items[0].stringProp, "String Test"); 194 | deepEqual(viewmodel.items()[0].number(), updatedModel.items[0].number, "String Test"); 195 | deepEqual(viewmodel.items()[0].date(), updatedModel.items[0].date, "String Test"); 196 | }); 197 | 198 | test("arrayChildId option swapped array item item", function () { 199 | var model, updatedModel, viewmodel, options, originalArrayItem; 200 | 201 | model = { 202 | items: [ 203 | { 204 | id: 4, 205 | stringProp: "test", 206 | number: 3, 207 | date: new Date("2/04/2001") 208 | } 209 | ] 210 | }; 211 | 212 | updatedModel = { 213 | items: [ 214 | { 215 | id: 5, 216 | stringProp: "test2", 217 | number: 6, 218 | date: new Date("12/04/2001") 219 | } 220 | ] 221 | }; 222 | 223 | options = { 224 | arrayChildId: { 225 | "{root}.items": "id" 226 | } 227 | } 228 | 229 | viewmodel = ko.viewmodel.fromModel(model, options); 230 | 231 | originalArrayItem = viewmodel.items()[0]; 232 | deepEqual(originalArrayItem.id(), 4, "verify id before update"); 233 | 234 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 235 | 236 | deepEqual(viewmodel.items()[0] !== originalArrayItem, true, "verify not still same object"); 237 | deepEqual(viewmodel.items()[0].id(), 5, "verify id before update"); 238 | deepEqual(viewmodel.items().length, updatedModel.items.length, "Array Length Test"); 239 | deepEqual(viewmodel.items()[0].id(), updatedModel.items[0].id, "Array Item id Test"); 240 | deepEqual(viewmodel.items()[0].stringProp(), updatedModel.items[0].stringProp, "String Test"); 241 | deepEqual(viewmodel.items()[0].number(), updatedModel.items[0].number, "String Test"); 242 | deepEqual(viewmodel.items()[0].date(), updatedModel.items[0].date, "String Test"); 243 | }); 244 | test("array item item", function () { 245 | var model, updatedModel, viewmodel, options; 246 | 247 | model = { 248 | items: [ 249 | { 250 | stringProp: "test", 251 | number: 3, 252 | date: new Date("2/04/2001") 253 | } 254 | ] 255 | }; 256 | 257 | updatedModel = { 258 | items: [ 259 | { 260 | stringProp: "test2", 261 | number: 6, 262 | date: new Date("12/04/2001") 263 | } 264 | ] 265 | }; 266 | 267 | viewmodel = ko.viewmodel.fromModel(model); 268 | 269 | deepEqual(viewmodel.items().length, model.items.length, "Array Length Test"); 270 | deepEqual(viewmodel.items()[0].stringProp(), model.items[0].stringProp, "String Test"); 271 | deepEqual(viewmodel.items()[0].number(), model.items[0].number, "String Test"); 272 | deepEqual(viewmodel.items()[0].date(), model.items[0].date, "String Test"); 273 | 274 | ko.viewmodel.updateFromModel(viewmodel, updatedModel); 275 | 276 | deepEqual(viewmodel.items().length, updatedModel.items.length, "Array Length Test"); 277 | deepEqual(viewmodel.items()[0].stringProp(), updatedModel.items[0].stringProp, "String Test"); 278 | deepEqual(viewmodel.items()[0].number(), updatedModel.items[0].number, "String Test"); 279 | deepEqual(viewmodel.items()[0].date(), updatedModel.items[0].date, "String Test"); 280 | }); -------------------------------------------------------------------------------- /Tests/fromModel-Mapping-qunit-tests.js: -------------------------------------------------------------------------------- 1 | module("fromModel Mapping", { 2 | setup: function () { 3 | //ko.viewmodel.options.logging = true; 4 | }, 5 | teardown: function () { 6 | //ko.viewmodel.options.logging = false; 7 | } 8 | }); 9 | 10 | test("Extend full path", function () { 11 | var model, viewmodel, modelResult; 12 | 13 | model = { 14 | test: { 15 | stringProp: "test" 16 | } 17 | }; 18 | 19 | var customMapping = { 20 | extend: { 21 | "{root}.test.stringProp": function (obj) { 22 | obj.repeat = ko.computed(function () { 23 | return obj() + obj(); 24 | }); 25 | return obj; 26 | } 27 | } 28 | }; 29 | 30 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 31 | 32 | deepEqual(viewmodel.test.stringProp.repeat(), viewmodel.test.stringProp() + viewmodel.test.stringProp()); 33 | }); 34 | 35 | test("Extend full path with shared", function () { 36 | var model, viewmodel, modelResult; 37 | 38 | model = { 39 | test: { 40 | stringProp: "test" 41 | }, 42 | otherTest: { 43 | stringProp: "test" 44 | } 45 | }; 46 | 47 | var customMapping = { 48 | extend: { 49 | "{root}.test.stringProp": "repeat", 50 | "{root}.otherTest.stringProp": { 51 | map: "repeat" 52 | } 53 | }, 54 | shared: { 55 | "repeat": function (obj) { 56 | obj.repeat = ko.computed(function () { 57 | return obj() + obj(); 58 | }); 59 | return obj; 60 | } 61 | } 62 | }; 63 | 64 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 65 | 66 | deepEqual(viewmodel.test.stringProp.repeat(), viewmodel.test.stringProp() + viewmodel.test.stringProp()); 67 | deepEqual(viewmodel.otherTest.stringProp.repeat(), viewmodel.otherTest.stringProp() + viewmodel.otherTest.stringProp()); 68 | }); 69 | 70 | test("Extend object property path", function () { 71 | var model, viewmodel, modelResult; 72 | 73 | model = { 74 | test: { 75 | stringProp: "test" 76 | } 77 | }; 78 | 79 | var customMapping = { 80 | extend: { 81 | "test.stringProp": function (obj) { 82 | obj.repeat = ko.computed(function () { 83 | return obj() + obj(); 84 | }); 85 | return obj; 86 | } 87 | } 88 | }; 89 | 90 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 91 | 92 | deepEqual(viewmodel.test.stringProp.repeat(), viewmodel.test.stringProp() + viewmodel.test.stringProp()); 93 | }); 94 | 95 | test("Extend property path", function () { 96 | var model, viewmodel, modelResult; 97 | 98 | model = { 99 | stringProp: "test" 100 | }; 101 | 102 | var customMapping = { 103 | extend: { 104 | "stringProp": function (obj) { 105 | obj.repeat = ko.computed(function () { 106 | return obj() + obj(); 107 | }); 108 | return obj; 109 | } 110 | } 111 | }; 112 | 113 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 114 | 115 | deepEqual(viewmodel.stringProp.repeat(), viewmodel.stringProp() + viewmodel.stringProp()); 116 | }); 117 | 118 | test("Extend full path wins over object property path", function () { 119 | var model, viewmodel, modelResult; 120 | 121 | model = { 122 | test: { 123 | stringProp: "test" 124 | } 125 | }; 126 | 127 | var customMapping = { 128 | extend: { 129 | "{root}.test.stringProp": function (obj) { 130 | obj.repeat = ko.computed(function () { 131 | return obj() + obj(); 132 | }); 133 | return obj; 134 | }, 135 | "test.stringProp": function (obj) { 136 | return null; 137 | } 138 | } 139 | }; 140 | 141 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 142 | 143 | deepEqual(viewmodel.test.stringProp.repeat(), viewmodel.test.stringProp() + viewmodel.test.stringProp()); 144 | }); 145 | 146 | test("Extend full path wins over property path", function () { 147 | var model, viewmodel, modelResult; 148 | 149 | model = { 150 | test: { 151 | stringProp: "test" 152 | } 153 | }; 154 | 155 | var customMapping = { 156 | extend: { 157 | "{root}.test.stringProp": function (obj) { 158 | obj.repeat = ko.computed(function () { 159 | return obj() + obj(); 160 | }); 161 | return obj; 162 | }, 163 | "stringProp": function (obj) { 164 | return null; 165 | } 166 | } 167 | }; 168 | 169 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 170 | 171 | deepEqual(viewmodel.test.stringProp.repeat(), viewmodel.test.stringProp() + viewmodel.test.stringProp()); 172 | }); 173 | 174 | 175 | test("Extend array array-item property path", function () { 176 | var model, viewmodel, modelResult, actual, expected; 177 | 178 | model = { 179 | items: [{ 180 | test: { 181 | stringProp: "test" 182 | } 183 | }] 184 | }; 185 | 186 | var customMapping = { 187 | extend: { 188 | "items[i].test": function (obj) { 189 | obj.repeat = ko.computed(function () { 190 | return obj.stringProp() + obj.stringProp(); 191 | }); 192 | return obj; 193 | } 194 | } 195 | }; 196 | 197 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 198 | 199 | actual = viewmodel.items()[0].test.repeat(); 200 | expected = model.items[0].test.stringProp + model.items[0].test.stringProp; 201 | 202 | deepEqual(actual, expected); 203 | }); 204 | 205 | test("Extend array array-item property path wins over array-item property path", function () { 206 | var model, viewmodel, modelResult, actual, expected; 207 | 208 | model = { 209 | items: [{ 210 | test: { 211 | stringProp: "test" 212 | } 213 | }] 214 | }; 215 | 216 | var customMapping = { 217 | extend: { 218 | "[i].test": function (obj) { 219 | return null; 220 | }, 221 | "items[i].test": function (obj) { 222 | obj.repeat = ko.computed(function () { 223 | return obj.stringProp() + obj.stringProp(); 224 | }); 225 | return obj; 226 | } 227 | } 228 | }; 229 | 230 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 231 | 232 | actual = viewmodel.items()[0].test.repeat(); 233 | expected = model.items[0].test.stringProp + model.items[0].test.stringProp; 234 | 235 | deepEqual(actual, expected); 236 | }); 237 | 238 | test("Extend array array-item property path wins over property path", function () { 239 | var model, viewmodel, modelResult, actual, expected; 240 | 241 | model = { 242 | items: [{ 243 | test: { 244 | stringProp: "test" 245 | } 246 | }] 247 | }; 248 | 249 | var customMapping = { 250 | extend: { 251 | "test": function (obj) { 252 | return null; 253 | }, 254 | "items[i].test": function (obj) { 255 | obj.repeat = ko.computed(function () { 256 | return obj.stringProp() + obj.stringProp(); 257 | }); 258 | return obj; 259 | } 260 | } 261 | }; 262 | 263 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 264 | 265 | actual = viewmodel.items()[0].test.repeat(); 266 | expected = model.items[0].test.stringProp + model.items[0].test.stringProp; 267 | 268 | deepEqual(actual, expected); 269 | }); 270 | 271 | test("Extend all array items", function () { 272 | var model, viewmodel, modelResult, actual, expected; 273 | 274 | model = { 275 | items: [{ 276 | test: { 277 | stringProp: "test" 278 | } 279 | }] 280 | }; 281 | 282 | var customMapping = { 283 | extend: { 284 | "[i]": function (obj) { 285 | obj.IsNew = false; 286 | } 287 | } 288 | }; 289 | 290 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 291 | 292 | actual = viewmodel.items()[0].IsNew; 293 | expected = false; 294 | 295 | deepEqual(actual, expected); 296 | }); 297 | 298 | 299 | test("Extended Array Push with Map", function () { 300 | var model, viewmodel, modelResult, actual, expected; 301 | 302 | model = { 303 | items: [{ 304 | test: { 305 | stringProp: "test" 306 | }, 307 | id: 1259 308 | }] 309 | }; 310 | 311 | var customMapping = { 312 | extend: { 313 | "{root}.items[i]": { 314 | map: function (obj) { 315 | obj.IsNew = (obj.id() > 0) ? false : true; 316 | }, 317 | unmap: function (obj, vm) { 318 | if (vm) {//not using this param but test will fail if not available, easy way to verify that it is passe in 319 | delete obj.IsNew; 320 | } 321 | return obj; 322 | } 323 | } 324 | } 325 | }; 326 | 327 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 328 | 329 | deepEqual(viewmodel.items()[0].IsNew, false, "Extend logic - object with id is not new");//1 330 | 331 | viewmodel.items.pushFromModel({ 332 | test: { 333 | stringProp: "test" 334 | }, 335 | id: null 336 | }); 337 | 338 | deepEqual(viewmodel.items()[1].IsNew, true, "Extend logic applied to pushFromModel - object with id OF null is not new");//2 339 | 340 | actual = viewmodel.items()[0]; 341 | expected = viewmodel.items.pop() 342 | 343 | notStrictEqual(actual, expected, "Pop does not change object");//3 344 | 345 | viewmodel.items.push(expected); 346 | 347 | actual = viewmodel.items()[0]; 348 | expected = viewmodel.items.pop() 349 | 350 | notStrictEqual(actual, expected, "Pushing and popping does not change object");//4 351 | 352 | expected = model.items[0]; 353 | actual = viewmodel.items.popToModel(); 354 | 355 | deepEqual(actual, expected, "popToModel calls unmap and removes IsNew property");//5 356 | 357 | }); 358 | 359 | test("Extended Array Push without Map", function () { 360 | var model, viewmodel, modelResult, actual, expected; 361 | 362 | model = { 363 | items: [{ 364 | test: { 365 | stringProp: "test" 366 | } 367 | }] 368 | }; 369 | 370 | var customMapping = { 371 | extend: { 372 | "[i]": function (obj) { 373 | obj.IsNew = false; 374 | } 375 | } 376 | }; 377 | 378 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 379 | 380 | viewmodel.items.push({ 381 | test: { 382 | stringProp: "test" 383 | } 384 | }, {map:false}); 385 | 386 | notEqual(viewmodel.items.pop(), viewmodel.items()[0]); 387 | }); 388 | 389 | test("Exclude full path", function () { 390 | var model, viewmodel, modelResult, actual, expected; 391 | 392 | model = { 393 | items: [{ 394 | test: { 395 | stringProp: "test" 396 | } 397 | }] 398 | }; 399 | 400 | var customMapping = { 401 | exclude: ["{root}.items[i].test"] 402 | }; 403 | 404 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 405 | modelResult = ko.viewmodel.toModel(viewmodel); 406 | 407 | deepEqual(viewmodel.items()[0].test, undefined, "Item Not Mapped"); 408 | }); 409 | 410 | test("Exclude full path wins over append object-property path", function () { 411 | var model, viewmodel, modelResult, actual, expected; 412 | 413 | model = { 414 | items: [{ 415 | test: { 416 | stringProp: "test" 417 | } 418 | }] 419 | }; 420 | 421 | var customMapping = { 422 | exclude: ["{root}.items[i].test"], 423 | append: ["items[i].test"] 424 | }; 425 | 426 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 427 | modelResult = ko.viewmodel.toModel(viewmodel); 428 | 429 | deepEqual(viewmodel.items()[0].test, undefined, "Item Not Mapped"); 430 | }); 431 | 432 | test("Exclude object-property path wins over append property path", function () { 433 | var model, viewmodel, modelResult, actual, expected; 434 | 435 | model = { 436 | items: [{ 437 | test: { 438 | stringProp: "test" 439 | } 440 | }] 441 | }; 442 | 443 | var customMapping = { 444 | exclude: ["items[i].test"], 445 | append: ["test"] 446 | }; 447 | 448 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 449 | modelResult = ko.viewmodel.toModel(viewmodel); 450 | 451 | deepEqual(viewmodel.items()[0].test, undefined, "Item Not Mapped"); 452 | }); 453 | 454 | 455 | test("Same path Last in wins", function () { 456 | var model, viewmodel, modelResult, actual, expected; 457 | 458 | model = { 459 | items: [{ 460 | test: { 461 | stringProp: "test" 462 | } 463 | }] 464 | }; 465 | 466 | var customMapping = { 467 | exclude: ["items[i].test"], 468 | append: ["items[i].test"] 469 | }; 470 | 471 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 472 | modelResult = ko.viewmodel.toModel(viewmodel); 473 | 474 | notEqual(viewmodel.items()[0].test, undefined, "Item Not Mapped"); 475 | }); 476 | 477 | test("Exclude array item property path", function () { 478 | var model, viewmodel, modelResult, actual, expected; 479 | 480 | model = { 481 | items: [{ 482 | test: { 483 | stringProp: "test" 484 | } 485 | }] 486 | }; 487 | 488 | var customMapping = { 489 | exclude: ["items[i].test"] 490 | }; 491 | 492 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 493 | modelResult = ko.viewmodel.toModel(viewmodel); 494 | 495 | deepEqual(viewmodel.items()[0].test, undefined, "Item Not Mapped"); 496 | }); 497 | 498 | test("Append property", function () { 499 | var model, viewmodel, modelResult, actual, expected; 500 | 501 | model = { 502 | items: [{ 503 | test: { 504 | stringProp: "test" 505 | } 506 | }] 507 | }; 508 | 509 | var customMapping = { 510 | append: ["items[i]"] 511 | }; 512 | 513 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 514 | 515 | actual = viewmodel.items()[0].test.stringProp; 516 | expected = model.items[0].test.stringProp 517 | 518 | deepEqual(actual, expected, "Item Not Mapped"); 519 | }); 520 | 521 | test("Override array", function () { 522 | var model, viewmodel, modelResult, actual, expected; 523 | 524 | model = { 525 | items: [{ 526 | test: { 527 | stringProp: "test" 528 | } 529 | }] 530 | }; 531 | 532 | var customMapping = { 533 | override: ["[i]"] 534 | }; 535 | 536 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 537 | 538 | actual = viewmodel.items()[0].test.stringProp(); 539 | expected = model.items[0].test.stringProp 540 | 541 | deepEqual(actual, expected, "Item Not Mapped"); 542 | }); 543 | 544 | test("Override object", function () { 545 | var model, viewmodel, modelResult, actual, expected; 546 | 547 | model = { 548 | items: [{ 549 | test: { 550 | stringProp: "test" 551 | } 552 | }] 553 | }; 554 | 555 | var customMapping = { 556 | override: ["{root}"] 557 | }; 558 | 559 | 560 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 561 | 562 | actual = viewmodel.items()[0].test.stringProp(); 563 | expected = model.items[0].test.stringProp 564 | 565 | deepEqual(actual, expected, "Item Not Mapped"); 566 | }); 567 | 568 | test("Custom Success", function () { 569 | var model, viewmodel, modelResult, actual, expected; 570 | 571 | model = { 572 | items: [{ 573 | test: { 574 | stringProp: "test" 575 | } 576 | }] 577 | }; 578 | 579 | var customMapping = { 580 | custom: { 581 | "test": function (obj) { 582 | return obj ? true : false; 583 | } 584 | } 585 | }; 586 | 587 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 588 | 589 | deepEqual(viewmodel.items()[0].test, true, "Item Not Mapped"); 590 | }); 591 | 592 | test("Custom Success with shared", function () { 593 | var model, viewmodel, modelResult, actual, expected; 594 | 595 | model = { 596 | items: [{ 597 | test: { 598 | stringProp: "test" 599 | } 600 | }] 601 | }; 602 | 603 | var customMapping = { 604 | custom: { 605 | "test": "test" 606 | }, 607 | shared: { 608 | "test": function (obj) { 609 | return obj ? true : false; 610 | } 611 | } 612 | }; 613 | 614 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 615 | 616 | deepEqual(viewmodel.items()[0].test, true, "Item Not Mapped"); 617 | }); 618 | 619 | test("Custom Fail", function () { 620 | var model, viewmodel, modelResult, actual, expected; 621 | 622 | model = { 623 | items: [{ 624 | test: { 625 | stringProp: "test" 626 | } 627 | }] 628 | }; 629 | 630 | var customMapping = { 631 | custom: { 632 | "test": function (obj) { 633 | 634 | } 635 | } 636 | }; 637 | 638 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 639 | 640 | deepEqual(viewmodel.items()[0].test, undefined, "Item Not Mapped"); 641 | }); 642 | 643 | test("Custom Obsevable", function () { 644 | var model, viewmodel, modelResult, actual, expected; 645 | 646 | model = { 647 | items: [{ 648 | test: { 649 | stringProp: "test" 650 | } 651 | }] 652 | }; 653 | 654 | var customMapping = { 655 | custom: { 656 | "test": function (obj) { 657 | 658 | } 659 | } 660 | }; 661 | 662 | viewmodel = ko.viewmodel.fromModel(model, customMapping); 663 | 664 | deepEqual(viewmodel.items()[0].test, undefined, "Item Not Mapped"); 665 | }); 666 | 667 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Knockout Viewmodel - Cleaner, Faster, Better Knockout Mapping 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | View on GitHub 21 | 22 |

Knockout Viewmodel Plugin

23 |

Cleaner, Faster, Better Knockout Mapping

24 | 25 |
26 | Download this project as a .zip file 27 | Download this project as a tar.gz file 28 |
Download Latest Version
29 | (also on Nuget) 30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |

The Knockout Viewmodel Plugin (ko.viewmodel)

39 |
40 |

The fastest mapping plugin!

41 |

The knockout viewmodel plugin runs several times faster than the knockout mapping plugin. It also allows you to fine tune your viewmodel creation for even more speed. 42 | Now you can create complex observable viewmodels easily and with more structure and control than ever before! 43 |

44 |
45 |
46 |

You can trust that it works!!!

47 |

48 | There is a growing suite of unit-tests that ensures that ko.viewmodel works reliably. Bug fixes always start with one or more new unit test so reliability goes up over time. 49 |

50 |
51 |
52 |

Creating a simple viewmodel

53 |

This code creates an viewModel with an observable array of users whose properties are observable.

54 |

 55 | model = {//your model is normally provided via an ajax call
 56 |   users:[ 
 57 |         {firstName:"John", lastName:"Doe"},
 58 |         {firstName:"James", lastName:"Smith"}
 59 |     ]
 60 | };
 61 | 
 62 | viewmodel = ko.viewmodel.fromModel(model);
 63 | 			
64 |
65 |
66 |

Updating a viewmodel

67 | 68 |

If you need to update your viewmodel with more recent model data this can be done by calling updateFromModel.

69 | 70 |

 71 | ko.viewmodel.updateFromModel(viewmodel, updatedModel);
 72 |                 
73 | 74 |

This method optionally takes a third parameter, makeNoncontiguousObjectUpdates, which can be used to eliminate long running script errors in older browsers. When set to true each object will have an update function created for it which will be called via setTimeout. Given this makeNoncontiguousObjectUpdates should only be set to true if you are getting long running script errors in older browsers. An onComplete method allows you to pass in a callback to be fired when noncontiguous object updates are completed.

75 |

 76 | ko.viewmodel.updateFromModel(viewmodel, updatedModel, true).onComplete(function(){
 77 |     //respond to completion of update
 78 | });
 79 |                 
80 |

Note: the onComplete method is only available when makeNoncontiguousObjectUpdates is set to true.

81 |
82 |
83 |

Getting an updated model

84 | 85 |

At somepoint you'll need to get an updated model to send back up to the server. This can be done by calling toModel.

86 | 87 |

 88 | model = ko.viewmodel.toModel(viewmodel);
 89 |         
90 |
91 |
92 |

Extending your viewmodel

93 |

Now lets say that you want to extend each object in the users array with an isDeleted flag. You would do this by specifying an extend option. 94 |

95 |

 96 | options:{ 
 97 |     extend:{
 98 |         "{root}.users[i]": function(user){
 99 |             user.isDeleted = ko.observable(false);
100 |         }
101 |     }
102 | };
103 | 
104 | viewmodel = ko.viewmodel.fromModel(model,options));
105 | 
106 |

If we also wanted to add a delete method to an array we would use the following options:

107 |

108 | options:{ 
109 |     extend:{
110 |         "{root}.users[i]": function(user){
111 |             user.isDeleted= ko.observable(false);
112 |         },
113 |         "{root}.users": function(users){
114 |             users.Delete = function(user){
115 |                 user.isDeleted(true);
116 |             }
117 |         }
118 |     }
119 | };
120 | 
121 | 122 |
123 | 124 |
125 |

Processing Option Paths

126 | 127 |

Processing options are specified for an item by it's path. Every full path starts with {root}. Items in an array are referred to as [i]. So a path of "{root}.users[i].firstName" would be used to specify custom processing for the firstName property of every object in the users array. That is its full path name, but it's not necessary to refer to every item by it's -full path name. There are three ways of referencing a path:

128 | 129 |
    130 |
  • Full Path Name - Matches only the specific path, e.g. "{root}.users[i].firstName".
  • 131 |
  • Parent Child-Name - Matches the parent child combination specified. So "users[i].firstName" will only match the first name property on objects in the users array. Array items have to be referenced as ParentName[i].ChildName but for all other objects the proper reference is ParentName.ChildName
  • 132 |
  • Property Name - Matches every property with that name. So a partial path of "firstName" would match every firstName property in your model.
  • 133 |
134 |
135 |
136 |

Processing Options

137 | 138 |

139 | There six types of processing options: custom, append, exclude, extend, arrayChildId, and shared. 140 |

141 | 142 |
    143 |
  • custom - the path and all of it's children are processed only as you specify
  • 144 |
  • append - the path and it's children are appended as is
  • 145 |
  • exclude - the path is excluded from processing
  • 146 |
  • extend - the path and it's children are processed normally but then extended/modified as specified
  • 147 |
  • arrayChildId - used to identify the what property of the array's child objects should to identify them for update purposes
  • 148 |
  • shared - allows you to reduce duplicate definitions by creating named processing functions that can be used with extend and map.
  • 149 |
150 |

151 | The only processing types that can be used together on a path are extend and arrayChildId. If multiple processing options are specified 152 | for the same path only one will win. Which one will win is subject to internal processing logic that has been organized for performance reasons 153 | and should not be relied upon as it is subject to change in future versions. 154 |

155 |
156 |

extend - processing option

157 | With the extend processing option the path and it's children are processed normally but then extended/modified as specified. 158 |

159 | options:{ 
160 |     extend:{
161 |         "{root}.users[i]": function(user){
162 |             user.isDeleted = ko.observable(false);
163 |             return user;
164 |         }
165 |     }
166 | };
167 |         
168 |

169 | The value of the path is passed to the extend function after processing has been completed on the path and its children. Objects can be modified without being returned. 170 | Note: Whatever is returned from the extend function will be persisted and replace the default processing. 171 |

172 | There is also this alternate syntax if you need to do unmapping: 173 |

174 | options:{ 
175 |     custom:{
176 |         "{root}.users[i]": {
177 |             map:function(mapped){
178 |                 mapped.isDeleted= ko.observable(false);
179 |                 return mapped;
180 |             },
181 |             unmap:function(unmapped){
182 |             //Because isDeleted was an observable it was unwrapped and added to the model. 
183 |                 delete unmapped.isDeleted;
184 |                 return unmapped;
185 |             }
186 |         }
187 |     }
188 | };
189 |                     
190 | Functions added with extend will be removed when your model is created, however observables will not be and can be removed using unmap if they will cause 191 | problems with servers side serialization. It should be noted that in many cases serialization will drop unexpected properties instead of throwing an 192 | error, so this is not always necessary and would depend on your platform. 193 |
194 |
195 |

custom - processing option

196 | The custom processing gives you complete control over mapping and unmapping from model to viewmodel and back, though as in the example below you can 197 | make additional calls to viewmodel to make things simpler. One case this is often used for is in translating back and forth from a server format (think date values) or structure 198 | to a format or structure that is easier to work with in javascript. 199 |

200 | options:{ 
201 |     custom:{
202 |         "{root}.users[i]": function(user){
203 |             user.isDeleted= ko.observable(false);
204 |             return user;
205 |         }
206 |     }
207 | };
208 |          
209 | 210 | There is also this alternate syntax if you need to do unmapping 211 |

212 | options:{ 
213 |     custom:{
214 |         "{root}.users[i]": {
215 |             map:function(user){
216 |                 var mapped = ko.viewmodel.fromModel(user);
217 |                 mapped().isDeleted= ko.observable(false);
218 |                 return mapped;
219 |             },
220 |             unmap:function(user){
221 |                 var unmapped = ko.viewmodel.toModel(user);
222 |                 delete unmapped.isDeleted;
223 |                 return unmapped;
224 |             }
225 |         }
226 |     }
227 | };
228 |         
229 | In this case the users are passed through as is with the addition of an observable isDeleted flag. 230 |
231 | Note: 232 |
    233 |
  • Make sure to return something from your function, otherwise undefined will be returned as the result of custom.
  • 234 |
  • The object passed into map is the unaltered object from your viewmodel or model (depending on if you are calling fromModel or toModel).
  • 235 |
236 |
237 |
238 |

append - processing option

239 | With the append processing option the path and all of it's children are appended as is. 240 |

241 | options:{ 
242 |     append:["{root}.users[i]"]
243 | };
244 |         
245 | In this case none of the users have been altered and are appended unchanged. 246 |
247 |
248 |

exclude - processing option

249 | With the exclude processing option the path and it's children are excluded from processing and will not be included. 250 |

251 | options:{ 
252 |     exclude:["{root}.users[i].firstName"]
253 | };
254 |                 
255 | The firstName property is not included. 256 | 257 | When calling fromModel: 258 |
    259 |
  • the path specified will not be wrapped in an observable.
  • 260 |
261 | When calling toModel: 262 |
    263 |
  • if the path is for a computed value it will be unwrapped.
  • 264 |
  • if the path is for a non-observable value it will be included.
  • 265 |
266 |
267 |
268 |

arrayChildId - processing option

269 | The arrayChildId option allows you to flag what the id is on an object within an array of objects so that those objects can be updated and not replaced when updateFromModel is called. 270 | If an arrayChildId is not specified then when updated data is loaded all old items will be removed from the array and new items added, which will 271 | cause you to loose the state of those objects. The syntax for this is simple: 272 |

273 | options:{ 
274 |     arrayChildId:{
275 |         "{root}.users":"id"
276 |     }
277 | }
278 |                 
279 | But this will make more sense in context. What if you had an array of items for purchase and the selected items were added to your total. 280 | The code might look similar to this: 281 | 282 |

283 | var model = {
284 |     items:[
285 |         {
286 |             id:256889,
287 |             name:"item Name",
288 |             description:"Description of Item",
289 |             availble:3,
290 |             price:4.25
291 |         }
292 |     ]
293 | };
294 | 
295 | var viewmodel = ko.viewmodel(model, { 
296 |     arrayChildId:{//child item id
297 |         "{root}.items":"id"
298 |     },
299 |     extend:{
300 |         "{root}.items":function(items){//Toggle function added to array for better performance
301 |             items.ToggleSelect = function (item){
302 |                 item.selected(!item.selected);
303 |             }
304 |             return items;
305 |         },
306 |         "{root}.items[i]":function (item){
307 |             item.selected = ko.observable(false);
308 |         }
309 |     }
310 | }
311 |                 
312 | So now imagine the app is regularly pinging the server to for an updated model so that the number of items available is accurate. 313 | Without the arrayChildId option every time an update came back the array would be populated again and all of the items would be 314 | extended with selected set to false. With the arrayChildId specified selected state of all objects will be maintained. 315 |
316 |
317 |

shared - processing option

318 | This option allows you to define processing functions that can be reference by name in the extend and custom processing options, 319 | thus eliminating duplicate code. The following example shows a model for a movies with showtimes. The options extend both 320 | the movies and the showtimes with an observable property called selected. 321 |

322 | var model = {
323 |     movies:[
324 |         {
325 |             id:256889,
326 |             name:"item Name",
327 |             description:"Description of Item",
328 |             showtimes:[
329 |                 {start:"11:45am", duration:"120 minutes", soldOut:false, pricingType:"Matinee"},
330 |                 {start:"6:45pm", duration:"120 minutes", soldOut:false, pricingType:"Evening"}
331 |             ]
332 |         }
333 |     ]
334 | };
335 | 
336 | var options = { 
337 |     arrayChildId:{//child item id
338 |         "{root}.movies":"id"
339 |     },
340 |     extend:{
341 |         "{root}.movies[i]":"ExtendWithSelectable",//Reference shared function by name
342 |         "{root}.movies[i].showtimes[i]": function(showtime){
343 | 
344 |             //Reference shared function directly
345 |             options.shared.ExtendWithSelectable(showtime);
346 | 
347 |             showtime.price = ko.computed(function(){
348 |                 if(showtime.pricingType = "Matinee"){
349 |                     return 6.75;
350 |                 }
351 |                 else if(showtime.pricingType = "Evening"){
352 |                     return 10.25;
353 |                 }
354 |             };
355 |         }
356 |     },
357 |     shared:{
358 |         ExtendWithSelectable:function (item){
359 |             item.selected = ko.observable(false);
360 |             return item;
361 |         }
362 |     }
363 | }
364 | 
365 | var viewmodel = ko.viewmodel(model, options);
366 |                 
367 | 368 | Another common use of this functionality would be if you had date, currency, or other formatting that needed to be applied to a large number of item. Or 369 | perhaps for a large number of fields you wanted "N/A" to be displayed if their were no values. This functionality could be defined once in shared 370 | and reference in many places by name. 371 |
372 |
373 |
374 |

Global Options

375 |

376 | Global options affect all calls to ko.viewmodel and are access and set via ko.viewmodel.options. Currently there are two global options: 377 |

378 | 379 |
    380 |
  • logging - (default:false) logs viewmodel processing to the console, including all paths processed and mapping options applied.
  • 381 |
  • makeChildArraysObservable - (default:true) determines if nested arrays are converted to observable arrays. 382 | Set to false for compatibility with original mapping plugin or to improve performance. 383 |
  • 384 |
385 |
386 |
387 |

Observable Array Mapping Methods

388 | Once an observable array has been mapped, items which need to be added and removed can be processed through viewmodel mapping using the following 389 | methods which are added to the observable array. 390 | 391 |
    392 |
  • pushFromModel - modifies the object passed in according to the original mapping options, and calls array.push
  • 393 |
  • unshiftFromModel - modifies the object passed in according to the original mapping options and calls array.unshift
  • 394 |
  • popToModel - calls array.pop and unmaps and returns the object according to the original mapping options
  • 395 |
  • shiftToModel - calls array.shift and unmaps and returns the object according to the original mapping options
  • 396 |
397 |
398 | 399 |
400 |

Think you've found a bug?

401 | If you think you've found a bug please submit a new issue ticket . 402 | All bug fixes start with failing unit tests so including one will help speed things along... thanks! 403 |
404 | 405 |
406 | 407 | 408 |
409 | 410 | 411 | 417 | 418 | 419 | 420 | 421 | 422 | -------------------------------------------------------------------------------- /nuget/2.0.0/knockout.viewmodel.2.0.0.js: -------------------------------------------------------------------------------- 1 | /*ko.viewmodel.js - version 2.0.0 2 | * Copyright 2013, Dave Herren http://coderenaissance.github.com/knockout.viewmodel/ 3 | * License: MIT (http://www.opensource.org/licenses/mit-license.php)*/ 4 | /*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 */ 5 | /*global ko:false */ 6 | 7 | //The following recursive algorithms, functions which call themselves, but are conceptually just loops 8 | //Like all loops when executed over a large enough number of items every statement executed bears a noticible load 9 | //Performance is of key concern in this project, so in many cases terse code is used to reduce the number of statements executed 10 | //which is especially important in older versions of IE; Given equal performance less terse code is to be prefered. 11 | ko.viewmodel = (function () { 12 | //Module declarations. For increased compression with simple settings on the closure compiler, 13 | //the ko functions are stored in variables. These variable names will be shortened by the compiler, 14 | //whereas references to ko would not be. There is also a performance savings from this. 15 | var unwrap = ko.utils.unwrapObservable, 16 | isObservable = ko.isObservable, 17 | makeObservable = ko.observable, 18 | makeObservableArray = ko.observableArray, 19 | rootContext = { name: "{root}", parent: "{root}", full: "{root}" }, 20 | fnLog, makeChildArraysObservable, 21 | badResult = function fnBadResult() { }; 22 | 23 | //Gets settings for the specified path 24 | function GetPathSettings(settings, context) { 25 | //Settings for more specific paths are chosen over less specific ones. 26 | var pathSettings = settings ? settings[context.full] || settings[context.parent] || settings[context.name] || {} : {}; 27 | if (fnLog) fnLog(context, pathSettings, settings);//log what mapping will be used 28 | return pathSettings; 29 | } 30 | 31 | //Converts options into a dictionary of path settings 32 | //This allows for path settings to be looked up efficiently 33 | function GetPathSettingsDictionary(options) { 34 | var result = {}, shared = options ? options.shared || {} : {}, 35 | settings, fn, index, key, length, settingType, childName, child; 36 | for (settingType in options) { 37 | settings = options[settingType] || {}; 38 | //Settings can either be dictionaries(associative arrays) or arrays 39 | //ignore shared option... contains functions that can be assigned by name 40 | if (settingType === "shared") continue; 41 | else if (settings instanceof Array) {//process array list for append and exclude 42 | for (index = 0, length = settings.length; index < length; index++) { 43 | key = settings[index]; 44 | result[key] = result[key] || {}; 45 | result[key][settingType] = true; 46 | result[key].settingType = result[key].settingType ? "multiple" : settingType; 47 | } 48 | } 49 | else if(settings.constructor === Object){//process associative array for extend and map 50 | for (key in settings) { 51 | result[key] = result[key] || {}; 52 | fn = settings[key]; 53 | fn = settingType !== "arrayChildId" && fn && fn.constructor === String && shared[fn] ? shared[fn] : fn; 54 | if (fn && fn.constructor === Object) {//associative array for map/unmap passed in instead of map function 55 | for (childName in fn) { 56 | //if children of fn are strings then replace with shared function if available 57 | if ((child = fn[childName]) && (child.constructor == String) && shared[child]) { 58 | fn[childName] = shared[child]; 59 | } 60 | } 61 | } 62 | result[key][settingType] = fn; 63 | result[key].settingType = result[key].settingType ? "multiple" : settingType; 64 | 65 | } 66 | } 67 | } 68 | return result; 69 | } 70 | 71 | function isNullOrUndefined(obj) {//checks if obj is null or undefined 72 | return obj === null || obj === undefined; 73 | } 74 | 75 | //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. 76 | //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. 77 | function isPrimativeOrDate(obj) { 78 | return obj === null || obj === undefined || obj.constructor === String || obj.constructor === Number || obj.constructor === Boolean || obj instanceof Date; 79 | } 80 | 81 | function fnRecursiveFrom(modelObj, settings, context) { 82 | var temp, result, p, length, idName, newContext, customPathSettings, extend, optionProcessed, 83 | pathSettings = GetPathSettings(settings, context); 84 | 85 | if (fnLog) {//Log object being mapped 86 | fnLog(context); 87 | } 88 | 89 | if (customPathSettings = pathSettings.custom) { 90 | optionProcessed = true; 91 | //custom can either be specified as a single map function or as an 92 | //object with map and unmap properties 93 | if (typeof customPathSettings === "function") { 94 | result = customPathSettings(modelObj); 95 | } 96 | else { 97 | result = customPathSettings.map(modelObj); 98 | if (!isNullOrUndefined(result)) {//extend object with mapping info where possible 99 | result.___$mapCustom = customPathSettings.map;//preserve map function for updateFromModel calls 100 | if (customPathSettings.unmap) {//perserve unmap function for toModel calls 101 | result.___$unmapCustom = customPathSettings.unmap; 102 | } 103 | } 104 | } 105 | } 106 | else if (pathSettings.append) {//append property 107 | optionProcessed = true; 108 | //Q:Can't mark null or undefined as appended, all others are ok 109 | if (!isNullOrUndefined(modelObj)) { 110 | modelObj.___$appended = undefined; 111 | } 112 | result = modelObj;//append 113 | } 114 | else if (pathSettings.exclude) { 115 | optionProcessed = true; 116 | return badResult; 117 | } 118 | else if (isPrimativeOrDate(modelObj)) { 119 | //primative and date children of arrays aren't mapped... all others are 120 | result = context.parentIsArray ? modelObj : makeObservable(modelObj); 121 | } 122 | else if (modelObj instanceof Array) { 123 | result = []; 124 | 125 | for (p = 0, length = modelObj.length; p < length; p++) { 126 | result[p] = fnRecursiveFrom(modelObj[p], settings, { 127 | name: "[i]", parent: context.name + "[i]", full: context.full + "[i]", parentIsArray: true 128 | }); 129 | } 130 | 131 | //only makeObservableArray extend with mapping functions if it's not a nested array or mapping compatabitlity is off 132 | if (!context.parentIsArray || makeChildArraysObservable) { 133 | 134 | newContext = { name: "[i]", parent: context.name + "[i]", full: context.full + "[i]", parentIsArray: true }; 135 | result = makeObservableArray(result); 136 | 137 | //if available add id name to object so it can be accessed later when updating children 138 | if (idName = pathSettings.arrayChildId) { 139 | result.___$childIdName = idName; 140 | } 141 | 142 | //wrap array methods for adding and removing items in functions that 143 | //close over settings and context allowing the objects and their children to be correctly mapped. 144 | result.pushFromModel = function (item) { 145 | item = fnRecursiveFrom(item, settings, newContext); 146 | result.push(item); 147 | }; 148 | result.unshiftFromModel = function (item) { 149 | item = fnRecursiveFrom(item, settings, newContext); 150 | result.unshift(item); 151 | }; 152 | result.popToModel = function (item) { 153 | item = result.pop(); 154 | return fnRecursiveTo(item, newContext); 155 | }; 156 | result.shiftToModel = function (item) { 157 | item = result.shift(); 158 | return fnRecursiveTo(item, newContext); 159 | }; 160 | } 161 | 162 | } 163 | else if (modelObj.constructor === Object) { 164 | result = {}; 165 | for (p in modelObj) { 166 | temp = fnRecursiveFrom(modelObj[p], settings, {//call recursive from on each child property 167 | name: p, 168 | parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, 169 | full: context.full + "." + p 170 | }); 171 | 172 | if (temp !== badResult) {//properties that couldn't be mapped return badResult 173 | result[p] = temp; 174 | } 175 | } 176 | } 177 | 178 | 179 | if (!optionProcessed && (extend = pathSettings.extend)) { 180 | if (typeof extend === "function") {//single map function specified 181 | //Extend can either modify the mapped value or replace it 182 | //Falsy values assumed to be undefined 183 | result = extend(result) || result; 184 | } 185 | else if (extend.constructor === Object) {//map and/or unmap were specified as part of object 186 | if (typeof extend.map === "function") { 187 | result = extend.map(result) || result;//use map to get result 188 | } 189 | 190 | if (typeof extend.unmap === "function") { 191 | result.___$unmapExtend = extend.unmap;//store unmap for use by toModel 192 | } 193 | } 194 | } 195 | return result; 196 | } 197 | 198 | function fnRecursiveTo(viewModelObj, context) { 199 | var result, p, length, temp, unwrapped = unwrap(viewModelObj), child, recursiveResult, 200 | wasWrapped = (viewModelObj !== unwrapped);//this works because unwrap observable calls isObservable and returns the object unchanged if not observable 201 | 202 | if (fnLog) { 203 | fnLog(context);//log object being unmapped 204 | } 205 | 206 | if (!wasWrapped && viewModelObj && viewModelObj.constructor === Function) {//Exclude functions 207 | return badResult; 208 | } 209 | else if (viewModelObj && viewModelObj.___$unmapCustom) {//Defer to customUnmapping where specified 210 | result = viewModelObj.___$unmapCustom(viewModelObj); 211 | } 212 | else if ((wasWrapped && isPrimativeOrDate(unwrapped)) || isNullOrUndefined(unwrapped) || unwrapped.hasOwnProperty("___$appended")) { 213 | //return null, undefined, appended values, and wrapped primativish values as is 214 | result = unwrapped; 215 | } 216 | else if (unwrapped instanceof Array) {//create new array to return and add unwrapped values to it 217 | result = []; 218 | for (p = 0, length = unwrapped.length; p < length; p++) { 219 | result[p] = fnRecursiveTo(unwrapped[p], { 220 | name: "[i]", parent: context.name + "[i]", full: context.full + "[i]" 221 | }); 222 | } 223 | } 224 | else if (unwrapped.constructor === Object) {//create new object to return and add unwrapped values to it 225 | result = {}; 226 | for (p in unwrapped) { 227 | if (p.substr(0, 2) !== "___$") {//ignore all properties starting with the magic string as internal 228 | child = unwrapped[p]; 229 | if (!ko.isComputed(child) && !((temp = unwrap(child)) && temp.constructor === Function)) { 230 | 231 | recursiveResult = fnRecursiveTo(child, { 232 | name: p, 233 | parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, 234 | full: context.full + "." + p 235 | }); 236 | 237 | //if badResult wasn't returned then add property 238 | if (recursiveResult !== badResult) { 239 | result[p] = recursiveResult; 240 | } 241 | } 242 | } 243 | } 244 | } 245 | else { 246 | //If it wasn't wrapped and it's not a function then return it. 247 | if (!wasWrapped && (typeof unwrapped !== "function")) { 248 | result = unwrapped; 249 | } 250 | } 251 | 252 | if (viewModelObj && viewModelObj.___$unmapExtend) {//if available call extend unmap function 253 | result = viewModelObj.___$unmapExtend(result, viewModelObj); 254 | } 255 | 256 | return result; 257 | } 258 | 259 | function fnRecursiveUpdate(modelObj, viewModelObj, context) { 260 | var p, q, found, foundModels, modelId, idName, length, unwrapped = unwrap(viewModelObj), 261 | wasWrapped = (viewModelObj !== unwrapped), child, map, tempArray, childTemp; 262 | if (fnLog) fnLog(context);//Log object being updated 263 | 264 | if (wasWrapped && (isNullOrUndefined(unwrapped) ^ isNullOrUndefined(modelObj))) { 265 | //if you have an observable to update and either the new or old value is 266 | //null or undefined then update the observable 267 | viewModelObj(modelObj); 268 | } 269 | else if (modelObj && unwrapped && unwrapped.constructor == Object && modelObj.constructor === Object) { 270 | for (p in modelObj) {//loop through object properties and update them 271 | child = unwrapped[p]; 272 | if (!isNullOrUndefined(child) && viewModelObj.hasOwnProperty("___$appended")) { 273 | //update appended child for round trip to server... 274 | //this probably won't affect view in most cases, though it could 275 | //Q: what would be the work around if the user didn't want this updated? 276 | unwrapped[p] = modelObj; 277 | } 278 | else if (child && typeof child.___$mapCustom === "function") { 279 | if (isObservable(child)) { 280 | childTemp = child.___$mapCustom(modelObj[p])//get child value mapped by custom maping 281 | childTemp = unwrap(childTemp);//don't nest observables... what you want is the value from the customMapping 282 | child(childTemp);//update child; 283 | } 284 | else {//property wasn't observable? update it anyway for return to server 285 | unwrapped[p] = unwrapped[p].___$mapCustom(modelObj[p]); 286 | } 287 | } 288 | else if (isNullOrUndefined(modelObj[p]) && unwrapped[p] && unwrapped[p].constructor === Object) { 289 | //Replace null or undefined with object for round trip to server; probably won't affect the view 290 | //WORKAROUND: If values are going to switch between obj and null/undefined and the UI needs to be updated 291 | //then the user should use the extend option to wrap the object in an observable 292 | unwrapped[p] = modelObj[p]; 293 | } 294 | else {//Recursive update everything else 295 | fnRecursiveUpdate(modelObj[p], unwrapped[p], { 296 | name: p, 297 | parent: (context.name === "[i]" ? context.parentChildName : context.name) + "." + p, 298 | full: context.full + "." + p 299 | }); 300 | } 301 | } 302 | } 303 | else if (unwrapped && unwrapped instanceof Array) { 304 | if (idName = viewModelObj.___$childIdName) {//id is specified, create, update, and delete by id 305 | foundModels = []; 306 | for (p = modelObj.length - 1; p >= 0; p--) { 307 | found = false; 308 | modelId = modelObj[p][idName]; 309 | for (q = unwrapped.length - 1; q >= 0; q--) { 310 | if (modelId === unwrapped[q][idName]()) {//If updated model id equals viewmodel id then update viewmodel object with model data 311 | fnRecursiveUpdate(modelObj[p], unwrapped[q], { 312 | name: "[i]", parentChildName: context.name + "[i]", full: context.full + "[i]" 313 | }); 314 | found = true; 315 | foundModels[q] = true; 316 | break; 317 | } 318 | } 319 | if (!found) {//If not found in updated model then remove from viewmodel 320 | viewModelObj.splice(p, 1); 321 | } 322 | } 323 | for (p = modelObj.length - 1; p >= 0; p--) { 324 | if (!foundModels[p]) {//If found and updated in viewmodel then add to viewmodel 325 | viewModelObj.pushFromModel(modelObj[p]); 326 | } 327 | } 328 | } 329 | else {//no id specified, replace old array items with new array items 330 | tempArray = []; 331 | map = viewModelObj.___$mapCustom; 332 | if (typeof map === "function") {//update array with mapped objects, use indexer for performance 333 | for (p = 0, length = modelObj.length; p < length; p++) { 334 | tempArray[p] = modelObj[p]; 335 | } 336 | viewModelObj(map(tempArray)); 337 | } 338 | else {//Can't use indexer for assignment; have to preserve original mapping with push 339 | viewModelObj(tempArray); 340 | for (p = 0, length = modelObj ? modelObj.length : 0; p < length; p++) { 341 | viewModelObj.pushFromModel(modelObj[p]); 342 | } 343 | } 344 | } 345 | } 346 | else if (wasWrapped) {//If it makes it this far and it was wrapped then update it 347 | viewModelObj(modelObj); 348 | } 349 | } 350 | 351 | function initInternals(options, startMessage) { 352 | makeChildArraysObservable = options.makeChildArraysObservable; 353 | if (window.console && options.logging) { 354 | //if logging should be done then log start message and add logging function 355 | console.log(startMessage); 356 | 357 | //Updates the console with information about what has been mapped and how 358 | fnLog = function fnUpdateConsole(context, pathSettings, settings) { 359 | var msg; 360 | if (pathSettings && pathSettings.settingType) {//if a setting will be used log it 361 | //message reads: SettingType FullPath (matched: path that was matched) 362 | msg = pathSettings.settingType + " " + context.full + " (matched: '" + ( 363 | (settings[context.full] ? context.full : "") || 364 | (settings[context.parent] ? context.parent : "") || 365 | (context.name) 366 | ) + "')"; 367 | } else {//log that default mapping was used for the path 368 | msg = "default " + context.full; 369 | } 370 | console.log("- " + msg); 371 | }; 372 | } 373 | else { 374 | fnLog = undefined;//setting the fn to undefined makes it easy to test if logging should be done 375 | } 376 | } 377 | 378 | return { 379 | options: { 380 | makeChildArraysObservable: true, 381 | logging: false 382 | }, 383 | fromModel: function fnFromModel(model, options) { 384 | var settings = GetPathSettingsDictionary(options); 385 | initInternals(this.options, "Mapping From Model"); 386 | return fnRecursiveFrom(model, settings, rootContext); 387 | }, 388 | toModel: function fnToModel(viewmodel) { 389 | initInternals(this.options, "Mapping To Model"); 390 | return fnRecursiveTo(viewmodel, rootContext); 391 | }, 392 | updateFromModel: function fnUpdateFromModel(viewmodel, model) { 393 | initInternals(this.options, "Update From Model"); 394 | return fnRecursiveUpdate(model, viewmodel, rootContext); 395 | } 396 | }; 397 | }()); 398 | -------------------------------------------------------------------------------- /nuget/2.0.1/knockout.viewmodel.2.0.1.js: -------------------------------------------------------------------------------- 1 | /*ko.viewmodel.js - version 2.0.1 2 | * Copyright 2013, Dave Herren http://coderenaissance.github.com/knockout.viewmodel/ 3 | * License: MIT (http://www.opensource.org/licenses/mit-license.php)*/ 4 | /*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 */ 5 | /*global ko:false */ 6 | 7 | (function () { 8 | //Module declarations. For increased compression with simple settings on the closure compiler, 9 | //the ko functions are stored in variables. These variable names will be shortened by the compiler, 10 | //whereas references to ko would not be. There is also a performance savings from this. 11 | var unwrap = ko.utils.unwrapObservable, 12 | isObservable = ko.isObservable, 13 | makeObservable = ko.observable, 14 | makeObservableArray = ko.observableArray, 15 | rootContext = { name: "{root}", parent: "{root}", full: "{root}" }, 16 | fnLog, makeChildArraysObservable, 17 | badResult = function fnBadResult() { }; 18 | 19 | //Gets settings for the specified path 20 | function getPathSettings(settings, context) { 21 | //Settings for more specific paths are chosen over less specific ones. 22 | var pathSettings = settings ? settings[context.full] || settings[context.parent] || settings[context.name] || {} : {}; 23 | if (fnLog) fnLog(context, pathSettings, settings);//log what mapping will be used 24 | return pathSettings; 25 | } 26 | 27 | //Converts options into a dictionary of path settings 28 | //This allows for path settings to be looked up efficiently 29 | function getPathSettingsDictionary(options) { 30 | var result = {}, shared = options ? options.shared || {} : {}, 31 | settings, fn, index, key, length, settingType, childName, child; 32 | for (settingType in options) { 33 | settings = options[settingType] || {}; 34 | //Settings can either be dictionaries(associative arrays) or arrays 35 | //ignore shared option... contains functions that can be assigned by name 36 | if (settingType === "shared") continue; 37 | else if (settings instanceof Array) {//process array list for append and exclude 38 | for (index = 0, length = settings.length; index < length; index++) { 39 | key = settings[index]; 40 | result[key] = result[key] || {}; 41 | result[key][settingType] = true; 42 | result[key].settingType = result[key].settingType ? "multiple" : settingType; 43 | } 44 | } 45 | else if(settings.constructor === Object){//process associative array for extend and map 46 | for (key in settings) { 47 | result[key] = result[key] || {}; 48 | fn = settings[key]; 49 | fn = settingType !== "arrayChildId" && fn && fn.constructor === String && shared[fn] ? shared[fn] : fn; 50 | if (fn && fn.constructor === Object) {//associative array for map/unmap passed in instead of map function 51 | for (childName in fn) { 52 | //if children of fn are strings then replace with shared function if available 53 | if ((child = fn[childName]) && (child.constructor == String) && shared[child]) { 54 | fn[childName] = shared[child]; 55 | } 56 | } 57 | } 58 | result[key][settingType] = fn; 59 | result[key].settingType = result[key].settingType ? "multiple" : settingType; 60 | 61 | } 62 | } 63 | } 64 | return result; 65 | } 66 | 67 | function isNullOrUndefined(obj) {//checks if obj is null or undefined 68 | return obj === null || obj === undefined; 69 | } 70 | 71 | //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. 72 | //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. 73 | function isPrimativeOrDate(obj) { 74 | return obj === null || obj === undefined || obj.constructor === String || obj.constructor === Number || obj.constructor === Boolean || obj instanceof Date; 75 | } 76 | 77 | function recrusiveFrom(modelObj, settings, context, pathSettings) { 78 | var temp, result, p, length, idName, newContext, customPathSettings, extend, optionProcessed, 79 | pathSettings = pathSettings || getPathSettings(settings, context), childPathSettings, childObj; 80 | 81 | if (customPathSettings = pathSettings.custom) { 82 | optionProcessed = true; 83 | //custom can either be specified as a single map function or as an 84 | //object with map and unmap properties 85 | if (typeof customPathSettings === "function") { 86 | result = customPathSettings(modelObj); 87 | } 88 | else { 89 | result = customPathSettings.map(modelObj); 90 | if (!isNullOrUndefined(result)) {//extend object with mapping info where possible 91 | result.___$mapCustom = customPathSettings.map;//preserve map function for updateFromModel calls 92 | if (customPathSettings.unmap) {//perserve unmap function for toModel calls 93 | result.___$unmapCustom = customPathSettings.unmap; 94 | } 95 | } 96 | } 97 | } 98 | else if (pathSettings.append) {//append property 99 | optionProcessed = true; 100 | result = modelObj;//append 101 | } 102 | else if (pathSettings.exclude) { 103 | optionProcessed = true; 104 | return badResult; 105 | } 106 | else if (isPrimativeOrDate(modelObj)) { 107 | //primative and date children of arrays aren't mapped... all others are 108 | result = context.parentIsArray ? modelObj : makeObservable(modelObj); 109 | } 110 | else if (modelObj instanceof Array) { 111 | result = []; 112 | 113 | for (p = 0, length = modelObj.length; p < length; p++) { 114 | result[p] = recrusiveFrom(modelObj[p], settings, { 115 | name: "[i]", parent: context.name + "[i]", full: context.full + "[i]", parentIsArray: true 116 | }); 117 | } 118 | 119 | //only makeObservableArray extend with mapping functions if it's not a nested array or mapping compatabitlity is off 120 | if (!context.parentIsArray || makeChildArraysObservable) { 121 | 122 | newContext = { name: "[i]", parent: context.name + "[i]", full: context.full + "[i]", parentIsArray: true }; 123 | result = makeObservableArray(result); 124 | 125 | //if available add id name to object so it can be accessed later when updating children 126 | if (idName = pathSettings.arrayChildId) { 127 | result.___$childIdName = idName; 128 | } 129 | 130 | //wrap array methods for adding and removing items in functions that 131 | //close over settings and context allowing the objects and their children to be correctly mapped. 132 | result.pushFromModel = function (item) { 133 | item = recrusiveFrom(item, settings, newContext); 134 | result.push(item); 135 | }; 136 | result.unshiftFromModel = function (item) { 137 | item = recrusiveFrom(item, settings, newContext); 138 | result.unshift(item); 139 | }; 140 | result.popToModel = function (item) { 141 | item = result.pop(); 142 | return recrusiveTo(item, newContext); 143 | }; 144 | result.shiftToModel = function (item) { 145 | item = result.shift(); 146 | return recrusiveTo(item, newContext); 147 | }; 148 | } 149 | 150 | } 151 | else if (modelObj.constructor === Object) { 152 | result = {}; 153 | for (p in modelObj) { 154 | newContext = { 155 | name: p, 156 | parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, 157 | full: context.full + "." + p 158 | }; 159 | childObj = modelObj[p]; 160 | childPathSettings = isPrimativeOrDate(childObj) ? getPathSettings(settings, newContext) : undefined; 161 | 162 | if (childPathSettings && childPathSettings.custom) {//primativish value w/ custom maping 163 | //since primative children cannot store their own custom functions, handle processing here and store them in the parent 164 | result.___$customChildren = result.___$customChildren || {}; 165 | result.___$customChildren[p] = childPathSettings.custom; 166 | 167 | if (typeof childPathSettings.custom === "function") { 168 | result[p] = childPathSettings.custom(modelObj[p]); 169 | } 170 | else { 171 | result[p] = childPathSettings.custom.map(modelObj[p]); 172 | } 173 | } 174 | else { 175 | temp = recrusiveFrom(childObj, settings, newContext, childPathSettings);//call recursive from on each child property 176 | 177 | if (temp !== badResult) {//properties that couldn't be mapped return badResult 178 | result[p] = temp; 179 | } 180 | 181 | } 182 | } 183 | } 184 | 185 | if (!optionProcessed && (extend = pathSettings.extend)) { 186 | if (typeof extend === "function") {//single map function specified 187 | //Extend can either modify the mapped value or replace it 188 | //Falsy values assumed to be undefined 189 | result = extend(result) || result; 190 | } 191 | else if (extend.constructor === Object) {//map and/or unmap were specified as part of object 192 | if (typeof extend.map === "function") { 193 | result = extend.map(result) || result;//use map to get result 194 | } 195 | 196 | if (typeof extend.unmap === "function") { 197 | result.___$unmapExtend = extend.unmap;//store unmap for use by toModel 198 | } 199 | } 200 | } 201 | return result; 202 | } 203 | 204 | function recrusiveTo(viewModelObj, context) { 205 | var result, p, length, temp, unwrapped = unwrap(viewModelObj), child, recursiveResult, 206 | wasWrapped = (viewModelObj !== unwrapped);//this works because unwrap observable calls isObservable and returns the object unchanged if not observable 207 | 208 | if (fnLog) { 209 | fnLog(context);//log object being unmapped 210 | } 211 | 212 | if (!wasWrapped && viewModelObj && viewModelObj.constructor === Function) {//Exclude functions 213 | return badResult; 214 | } 215 | else if (viewModelObj && viewModelObj.___$unmapCustom) {//Defer to customUnmapping where specified 216 | result = viewModelObj.___$unmapCustom(viewModelObj); 217 | } 218 | else if ((wasWrapped && isPrimativeOrDate(unwrapped)) || isNullOrUndefined(unwrapped) ) { 219 | //return null, undefined, values, and wrapped primativish values as is 220 | result = unwrapped; 221 | } 222 | else if (unwrapped instanceof Array) {//create new array to return and add unwrapped values to it 223 | result = []; 224 | for (p = 0, length = unwrapped.length; p < length; p++) { 225 | result[p] = recrusiveTo(unwrapped[p], { 226 | name: "[i]", parent: context.name + "[i]", full: context.full + "[i]" 227 | }); 228 | } 229 | } 230 | else if (unwrapped.constructor === Object) {//create new object to return and add unwrapped values to it 231 | result = {}; 232 | for (p in unwrapped) { 233 | if (p.substr(0, 4) !== "___$") {//ignore all properties starting with the magic string as internal 234 | if (viewModelObj.___$customChildren && viewModelObj.___$customChildren[p] && viewModelObj.___$customChildren[p].unmap) { 235 | result[p] = viewModelObj.___$customChildren[p].unmap(unwrapped[p]); 236 | } 237 | else { 238 | child = unwrapped[p]; 239 | if (!ko.isComputed(child) && !((temp = unwrap(child)) && temp.constructor === Function)) { 240 | 241 | recursiveResult = recrusiveTo(child, { 242 | name: p, 243 | parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, 244 | full: context.full + "." + p 245 | }); 246 | 247 | //if badResult wasn't returned then add property 248 | if (recursiveResult !== badResult) { 249 | result[p] = recursiveResult; 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | else { 257 | //If it wasn't wrapped and it's not a function then return it. 258 | if (!wasWrapped && (typeof unwrapped !== "function")) { 259 | result = unwrapped; 260 | } 261 | } 262 | 263 | if (viewModelObj && viewModelObj.___$unmapExtend) {//if available call extend unmap function 264 | result = viewModelObj.___$unmapExtend(result, viewModelObj); 265 | } 266 | 267 | return result; 268 | } 269 | 270 | function recursiveUpdate(modelObj, viewModelObj, context, parentObj) { 271 | var p, q, found, foundModels, modelId, idName, length, unwrapped = unwrap(viewModelObj), 272 | wasWrapped = (viewModelObj !== unwrapped), child, map, tempArray, childTemp, childMap; 273 | 274 | if (fnLog) { 275 | fnLog(context);//log object being unmapped 276 | } 277 | 278 | if (wasWrapped && (isNullOrUndefined(unwrapped) ^ isNullOrUndefined(modelObj))) { 279 | //if you have an observable to update and either the new or old value is 280 | //null or undefined then update the observable 281 | viewModelObj(modelObj); 282 | } 283 | else if (modelObj && unwrapped && unwrapped.constructor == Object && modelObj.constructor === Object) { 284 | for (p in modelObj) {//loop through object properties and update them 285 | 286 | if (viewModelObj.___$customChildren && viewModelObj.___$customChildren[p]) { 287 | childMap = viewModelObj.___$customChildren[p].map || viewModelObj.___$customChildren[p]; 288 | unwrapped[p] = childMap(modelObj[p]); 289 | } 290 | else{ 291 | child = unwrapped[p]; 292 | 293 | if (!wasWrapped && unwrapped.hasOwnProperty(p) && (isPrimativeOrDate(child) || (child && child.constructor === Array))) { 294 | unwrapped[p] = modelObj[p]; 295 | } 296 | else if (child && typeof child.___$mapCustom === "function") { 297 | if (isObservable(child)) { 298 | childTemp = child.___$mapCustom(modelObj[p])//get child value mapped by custom maping 299 | childTemp = unwrap(childTemp);//don't nest observables... what you want is the value from the customMapping 300 | child(childTemp);//update child; 301 | } 302 | else {//property wasn't observable? update it anyway for return to server 303 | unwrapped[p] = unwrapped[p].___$mapCustom(modelObj[p]); 304 | } 305 | } 306 | else if (isNullOrUndefined(modelObj[p]) && unwrapped[p] && unwrapped[p].constructor === Object) { 307 | //Replace null or undefined with object for round trip to server; probably won't affect the view 308 | //WORKAROUND: If values are going to switch between obj and null/undefined and the UI needs to be updated 309 | //then the user should use the extend option to wrap the object in an observable 310 | unwrapped[p] = modelObj[p]; 311 | } 312 | else {//Recursive update everything else 313 | recursiveUpdate(modelObj[p], unwrapped[p], { 314 | name: p, 315 | parent: (context.name === "[i]" ? context.parent : context.name) + "." + p, 316 | full: context.full + "." + p 317 | }, unwrapped); 318 | } 319 | } 320 | } 321 | } 322 | else if (unwrapped && unwrapped instanceof Array) { 323 | if (idName = viewModelObj.___$childIdName) {//id is specified, create, update, and delete by id 324 | foundModels = []; 325 | for (p = modelObj.length - 1; p >= 0; p--) { 326 | found = false; 327 | modelId = modelObj[p][idName]; 328 | for (q = unwrapped.length - 1; q >= 0; q--) { 329 | if (modelId === unwrapped[q][idName]()) {//If updated model id equals viewmodel id then update viewmodel object with model data 330 | recursiveUpdate(modelObj[p], unwrapped[q], { 331 | name: "[i]", parent: context.name + "[i]", full: context.full + "[i]" 332 | }); 333 | found = true; 334 | foundModels[q] = true; 335 | break; 336 | } 337 | } 338 | if (!found) {//If not found in updated model then remove from viewmodel 339 | viewModelObj.splice(p, 1); 340 | } 341 | } 342 | for (p = modelObj.length - 1; p >= 0; p--) { 343 | if (!foundModels[p]) {//If found and updated in viewmodel then add to viewmodel 344 | viewModelObj.pushFromModel(modelObj[p]); 345 | } 346 | } 347 | } 348 | else {//no id specified, replace old array items with new array items 349 | tempArray = []; 350 | map = viewModelObj.___$mapCustom; 351 | if (typeof map === "function") {//update array with mapped objects, use indexer for performance 352 | for (p = 0, length = modelObj.length; p < length; p++) { 353 | tempArray[p] = modelObj[p]; 354 | } 355 | viewModelObj(map(tempArray)); 356 | } 357 | else {//Can't use indexer for assignment; have to preserve original mapping with push 358 | viewModelObj(tempArray); 359 | for (p = 0, length = modelObj ? modelObj.length : 0; p < length; p++) { 360 | viewModelObj.pushFromModel(modelObj[p]); 361 | } 362 | } 363 | } 364 | } 365 | else if (wasWrapped) {//If it makes it this far and it was wrapped then update it 366 | viewModelObj(modelObj); 367 | } 368 | } 369 | 370 | function initInternals(options, startMessage) { 371 | makeChildArraysObservable = options.makeChildArraysObservable; 372 | if (window.console && options.logging) { 373 | //if logging should be done then log start message and add logging function 374 | console.log(startMessage); 375 | 376 | //Updates the console with information about what has been mapped and how 377 | fnLog = function fnUpdateConsole(context, pathSettings, settings) { 378 | var msg; 379 | if (pathSettings && pathSettings.settingType) {//if a setting will be used log it 380 | //message reads: SettingType FullPath (matched: path that was matched) 381 | msg = pathSettings.settingType + " " + context.full + " (matched: '" + ( 382 | (settings[context.full] ? context.full : "") || 383 | (settings[context.parent] ? context.parent : "") || 384 | (context.name) 385 | ) + "')"; 386 | } else {//log that default mapping was used for the path 387 | msg = "default " + context.full; 388 | } 389 | console.log("- " + msg); 390 | }; 391 | } 392 | else { 393 | fnLog = undefined;//setting the fn to undefined makes it easy to test if logging should be done 394 | } 395 | } 396 | 397 | ko.viewmodel = { 398 | options: { 399 | makeChildArraysObservable: true, 400 | logging: false 401 | }, 402 | fromModel: function fnFromModel(model, options) { 403 | var settings = getPathSettingsDictionary(options); 404 | initInternals(this.options, "Mapping From Model"); 405 | return recrusiveFrom(model, settings, rootContext); 406 | }, 407 | toModel: function fnToModel(viewmodel) { 408 | initInternals(this.options, "Mapping To Model"); 409 | return recrusiveTo(viewmodel, rootContext); 410 | }, 411 | updateFromModel: function fnUpdateFromModel(viewmodel, model) { 412 | initInternals(this.options, "Update From Model"); 413 | return recursiveUpdate(model, viewmodel, rootContext); 414 | } 415 | }; 416 | }()); 417 | --------------------------------------------------------------------------------