├── .gitignore ├── .vscode └── launch.json ├── HISTORY.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── bindings.js ├── helper.js ├── lzstring.js ├── parseBind.js ├── reactive-array.js ├── tracker.js ├── viewmodel-onUrl.js ├── viewmodel-property.js └── viewmodel.js └── test └── viewmodel.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | /*.iml 4 | /.idea 5 | /.git 6 | dist -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 3.1.3 2 | * ViewModel.property.afterUpdate now passes the previous value, not the new one (which can be accessed via the VM). 3 | 4 | # 3.1.2 5 | * Update vmChanged when share/mixin updates 6 | 7 | # 3.1.1 8 | * Set proper context (this) for share and mixin 9 | 10 | # 3.1.0 11 | * Add ViewModel.data() & .load(data) with the state of the entire app. 12 | 13 | # 3.0.0 14 | * shared properties keep the initial value when a component declares it. It makes more sense for the initial value of a shared property to be defined in the ViewModel.share 15 | 16 | # 2.4.1 17 | * Preset validations (min, max, equal, notEqual, between, notBetween) now coerce values. 18 | 19 | # 2.4.0 20 | * Add Inferno compatibility. See [Inferno](https://viewmodel.org/#BasicsInferno) for more information. 21 | 22 | # 2.3.0 23 | * Add component.child shortcut 24 | 25 | # 2.2.1 26 | * Reactivity now works with nested objects in properties. 27 | 28 | # 2.2.0 29 | * Add validating and validatingMessage to properties. A component will be invalid if it has a pending async validation. The validation message will be added to the invalid messages collection if it's pending. 30 | 31 | # 2.1.2 32 | * Fix throttle binding 33 | 34 | # 2.1.1 35 | * Fix issue with Meteor + shared 36 | 37 | # 2.1.0 38 | * Add `esc` binding 39 | 40 | # 2.0.2 41 | * Shortcircuit logical operators. 42 | 43 | # 2.0.1 44 | * Handle references to components and elements better. 45 | 46 | # 2.0.0 47 | * Added the [ref binding](https://viewmodel.org/#BindingsRef) for referencing elements and components. React's ref/refs will still work but it's on life support and likely to be deprecated by React in the future. 48 | 49 | # 1.0.3 50 | * Guard against window undefined 51 | 52 | # 1.0.2 53 | * Don't use window if it's not defined (fix for SSR) 54 | 55 | # 1.0.1 56 | * Fix React Native 57 | 58 | # 1.0.0 59 | * Hello World! 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 ManuelDeLeon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # viewmodel-react 2 | Create your React components with view models. 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewmodel-react", 3 | "version": "3.1.7", 4 | "description": "ViewModel for React", 5 | "main": "dist/viewmodel.js", 6 | "author": "Manuel De Leon (https://viewmodel.org/)", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "jest", 10 | "prepublish": 11 | "npm run prettier && babel --presets es2015 --out-dir dist src && npm run test", 12 | "prettier": 13 | "prettier --write \"{.,src/**,test/**}/*.{ts,tsx,js,css,scss,json}\"" 14 | }, 15 | "devDependencies": { 16 | "babel": "^5.0.0", 17 | "babel-preset-es2015": "^6.24.1", 18 | "jest": "^22.4.3", 19 | "jest-cli": "^22.4.3", 20 | "prettier": "^1.12.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/bindings.js: -------------------------------------------------------------------------------- 1 | const changeBinding = function(eb) { 2 | return ( 3 | eb.value || 4 | eb.check || 5 | eb.text || 6 | eb.html || 7 | eb.focus || 8 | eb.hover || 9 | eb.toggle || 10 | eb.if || 11 | eb.visible || 12 | eb.unless || 13 | eb.hide || 14 | eb.enable || 15 | eb.disable || 16 | eb.ref 17 | ); 18 | }; 19 | 20 | export default [ 21 | { 22 | name: "default", 23 | bind: function(bindArg) { 24 | if ( 25 | bindArg.bindName in bindArg.element && 26 | !(bindArg.element[bindArg.bindName] instanceof Function) 27 | ) { 28 | // It's an attribute or a component so don't add it as an event 29 | return; 30 | } 31 | const eventListener = function(event) { 32 | bindArg.setVmValue(event); 33 | }; 34 | 35 | bindArg.element.addEventListener(bindArg.bindName, eventListener); 36 | bindArg.component.vmDestroyed.push(() => { 37 | bindArg.element.removeEventListener(bindArg.bindName, eventListener); 38 | }); 39 | } 40 | }, 41 | { 42 | name: "value", 43 | events: { 44 | "input change": function(bindArg) { 45 | let newVal = bindArg.element.value; 46 | let vmVal = bindArg.getVmValue(); 47 | vmVal = vmVal == null ? "" : vmVal.toString(); 48 | if ( 49 | newVal !== vmVal || 50 | (bindArg.elementBind.throttle && 51 | (!bindArg.component[bindArg.bindValue].hasOwnProperty("nextVal") || 52 | newVal !== bindArg.component[bindArg.bindValue].nextVal)) 53 | ) { 54 | if (bindArg.elementBind.throttle) { 55 | bindArg.component[bindArg.bindValue].nextVal = newVal; 56 | } 57 | bindArg.setVmValue(newVal); 58 | } 59 | } 60 | }, 61 | autorun: function(bindArg) { 62 | let newVal = bindArg.getVmValue(); 63 | newVal = newVal == null ? "" : newVal.toString(); 64 | if (newVal !== bindArg.element.value) { 65 | bindArg.element.value = newVal; 66 | } 67 | var event = document.createEvent("HTMLEvents"); 68 | event.initEvent("change", true, false); 69 | bindArg.element.dispatchEvent(event); 70 | } 71 | }, 72 | 73 | { 74 | name: "check", 75 | events: { 76 | change: function(bindArg) { 77 | bindArg.setVmValue(bindArg.element.checked); 78 | } 79 | }, 80 | autorun: function(bindArg) { 81 | const vmValue = bindArg.getVmValue(); 82 | const elementCheck = bindArg.element.checked; 83 | if (elementCheck !== vmValue) { 84 | return (bindArg.element.checked = vmValue); 85 | } 86 | } 87 | }, 88 | { 89 | name: "check", 90 | selector: "input[type=radio]", 91 | bind: function(bindArg) { 92 | const name = bindArg.element.name; 93 | if (name) { 94 | const refs = bindArg.component.vmReferences; 95 | if (!refs.radios) { 96 | refs.radios = { [name]: [] }; 97 | } else if (!refs.radios[name]) { 98 | refs.radios[name] = []; 99 | } 100 | refs.radios[name].push(bindArg.element); 101 | } 102 | }, 103 | events: { 104 | change: function(bindArg) { 105 | const checked = bindArg.element.checked; 106 | bindArg.setVmValue(checked); 107 | const name = bindArg.element.name; 108 | if (checked && name) { 109 | const event = document.createEvent("HTMLEvents"); 110 | event.initEvent("change", true, false); 111 | const inputs = bindArg.component.vmReferences.radios[name]; 112 | inputs.forEach(input => { 113 | if (input !== bindArg.element) { 114 | input.dispatchEvent(event); 115 | } 116 | }); 117 | } 118 | } 119 | }, 120 | autorun: function(bindArg) { 121 | const vmValue = bindArg.getVmValue(); 122 | const elementCheck = bindArg.element.checked; 123 | if (elementCheck !== vmValue) { 124 | return (bindArg.element.checked = vmValue); 125 | } 126 | } 127 | }, 128 | { 129 | name: "group", 130 | selector: "input[type=checkbox]", 131 | events: { 132 | change: function(bindArg) { 133 | const vmValue = bindArg.getVmValue(); 134 | const elementValue = bindArg.element.value; 135 | if (bindArg.element.checked) { 136 | if (vmValue.indexOf(elementValue) < 0) { 137 | return vmValue.push(elementValue); 138 | } 139 | } else { 140 | return vmValue.remove(elementValue); 141 | } 142 | } 143 | }, 144 | autorun: function(bindArg) { 145 | const vmValue = bindArg.getVmValue(); 146 | const elementCheck = bindArg.element.checked; 147 | const elementValue = bindArg.element.value; 148 | const newValue = vmValue.indexOf(elementValue) >= 0; 149 | if (elementCheck !== newValue) { 150 | return (bindArg.element.checked = newValue); 151 | } 152 | } 153 | }, 154 | { 155 | name: "group", 156 | selector: "input[type=radio]", 157 | bind: function(bindArg) { 158 | const name = bindArg.element.name; 159 | if (name) { 160 | const refs = bindArg.component.vmReferences; 161 | if (!refs.radios) { 162 | refs.radios = { [name]: [] }; 163 | } else if (!refs.radios[name]) { 164 | refs.radios[name] = []; 165 | } 166 | refs.radios[name].push(bindArg.element); 167 | } 168 | }, 169 | events: { 170 | change: function(bindArg) { 171 | if (bindArg.element.checked) { 172 | bindArg.setVmValue(bindArg.element.value); 173 | 174 | const name = bindArg.element.name; 175 | if (name) { 176 | const event = document.createEvent("HTMLEvents"); 177 | event.initEvent("change", true, false); 178 | const inputs = bindArg.component.vmReferences.radios[name]; 179 | inputs.forEach(input => { 180 | if (input !== bindArg.element) { 181 | input.dispatchEvent(event); 182 | } 183 | }); 184 | } 185 | } 186 | } 187 | }, 188 | autorun: function(bindArg) { 189 | const vmValue = bindArg.getVmValue(); 190 | const elementValue = bindArg.element.value; 191 | return (bindArg.element.checked = vmValue === elementValue); 192 | } 193 | }, 194 | { 195 | name: "enter", 196 | events: { 197 | keyup: function(bindArg, event) { 198 | if (event.which === 13 || event.keyCode === 13) { 199 | bindArg.setVmValue(event); 200 | } 201 | } 202 | } 203 | }, 204 | { 205 | name: "esc", 206 | events: { 207 | keyup: function(bindArg, event) { 208 | if (event.which === 27 || event.keyCode === 27) { 209 | bindArg.setVmValue(event); 210 | } 211 | } 212 | } 213 | }, 214 | { 215 | name: "change", 216 | bind: function(bindArg) { 217 | const bindValue = changeBinding(bindArg.elementBind); 218 | bindArg.autorun(function(bindArg, c) { 219 | const newValue = bindArg.getVmValue(bindValue); 220 | if (!c.firstRun) { 221 | bindArg.setVmValue(newValue); 222 | } 223 | }); 224 | }, 225 | bindIf: function(bindArg) { 226 | return changeBinding(bindArg.elementBind); 227 | } 228 | }, 229 | { 230 | name: "hover", 231 | events: { 232 | mouseenter: function(bindArg) { 233 | bindArg.setVmValue(true); 234 | }, 235 | mouseleave: function(bindArg) { 236 | bindArg.setVmValue(false); 237 | } 238 | } 239 | }, 240 | { 241 | name: "focus", 242 | events: { 243 | focus: function(bindArg) { 244 | if (!bindArg.getVmValue()) bindArg.setVmValue(true); 245 | }, 246 | blur: function(bindArg) { 247 | if (bindArg.getVmValue()) bindArg.setVmValue(false); 248 | } 249 | }, 250 | autorun: function(bindArg) { 251 | const value = bindArg.getVmValue(); 252 | if ((bindArg.element === document.activeElement) !== value) { 253 | if (value) { 254 | bindArg.element.focus(); 255 | } else { 256 | bindArg.element.blur(); 257 | } 258 | } 259 | } 260 | }, 261 | { 262 | name: "toggle", 263 | events: { 264 | click: function(bindArg) { 265 | bindArg.setVmValue(!bindArg.getVmValue()); 266 | } 267 | } 268 | }, 269 | { 270 | name: "ref", 271 | bind: function(bindArg) { 272 | bindArg.component[bindArg.bindValue] = bindArg.element; 273 | } 274 | } 275 | ]; 276 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | const _tokens = { 2 | "**": function(a, b) { 3 | return Math.pow(a, b); 4 | }, 5 | "*": function(a, b) { 6 | return a() * b(); 7 | }, 8 | "/": function(a, b) { 9 | return a() / b(); 10 | }, 11 | "%": function(a, b) { 12 | return a() % b(); 13 | }, 14 | "+": function(a, b) { 15 | return a() + b(); 16 | }, 17 | "-": function(a, b) { 18 | return a() - b(); 19 | }, 20 | "<": function(a, b) { 21 | return a() < b(); 22 | }, 23 | "<=": function(a, b) { 24 | return a() <= b(); 25 | }, 26 | ">": function(a, b) { 27 | return a() > b(); 28 | }, 29 | ">=": function(a, b) { 30 | return a() >= b(); 31 | }, 32 | "==": function(a, b) { 33 | return a() == b(); 34 | }, 35 | "!==": function(a, b) { 36 | return a() !== b(); 37 | }, 38 | "===": function(a, b) { 39 | return a() === b(); 40 | }, 41 | "&&": function(a, b) { 42 | return a() && b(); 43 | }, 44 | "||": function(a, b) { 45 | return a() || b(); 46 | } 47 | }; 48 | 49 | const _tokenGroup = {}; 50 | for (let t in _tokens) { 51 | if (!_tokenGroup[t.length]) { 52 | _tokenGroup[t.length] = {}; 53 | } 54 | _tokenGroup[t.length][t] = 1; 55 | } 56 | 57 | export default class Helper { 58 | static isArray(arr) { 59 | return arr instanceof Array; 60 | } 61 | static isObject(obj) { 62 | return typeof obj === "object" && obj !== null && !(obj instanceof Date); 63 | } 64 | static isFunction(fun) { 65 | return fun && {}.toString.call(fun) === "[object Function]"; 66 | } 67 | static isString(str) { 68 | return typeof str === "string" || str instanceof String; 69 | } 70 | static isNumeric(n) { 71 | return !isNaN(parseFloat(n)) && isFinite(n); 72 | } 73 | 74 | static isQuoted(str) { 75 | return Helper.stringRegex.test(str); 76 | } 77 | static removeQuotes(str) { 78 | return str.substr(1, str.length - 2); 79 | } 80 | 81 | static isPrimitive(val) { 82 | return ( 83 | val === "true" || 84 | val === "false" || 85 | val === "null" || 86 | val === "undefined" || 87 | Helper.isNumeric(val) 88 | ); 89 | } 90 | 91 | static getPrimitive(val) { 92 | switch (val) { 93 | case "true": 94 | return true; 95 | case "false": 96 | return false; 97 | case "null": 98 | return null; 99 | case "undefined": 100 | return void 0; 101 | default: 102 | if (Helper.isNumeric(val)) { 103 | return parseFloat(val); 104 | } else { 105 | return val; 106 | } 107 | } 108 | } 109 | 110 | static firstToken(str) { 111 | var c, candidateToken, i, inQuote, j, k, len, length, token, tokenIndex; 112 | tokenIndex = -1; 113 | token = null; 114 | inQuote = null; 115 | for (i = j = 0, len = str.length; j < len; i = ++j) { 116 | c = str[i]; 117 | if (token) { 118 | break; 119 | } 120 | if (c === '"' || c === "'") { 121 | if (inQuote === c) { 122 | inQuote = null; 123 | } else if (!inQuote) { 124 | inQuote = c; 125 | } 126 | } else if (!inQuote && ~"+-*/%&|><=".indexOf(c)) { 127 | tokenIndex = i; 128 | for (length = k = 4; k >= 1; length = --k) { 129 | if (str.length > tokenIndex + length) { 130 | candidateToken = str.substr(tokenIndex, length); 131 | if (_tokenGroup[length] && _tokenGroup[length][candidateToken]) { 132 | token = candidateToken; 133 | break; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | return [token, tokenIndex]; 140 | } 141 | 142 | static getMatchingParenIndex(bindValue, parenIndexStart) { 143 | var currentChar, i, j, openParenCount, ref, ref1; 144 | if (!~parenIndexStart) { 145 | return -1; 146 | } 147 | openParenCount = 0; 148 | for ( 149 | i = j = ref = parenIndexStart + 1, ref1 = bindValue.length; 150 | ref <= ref1 ? j <= ref1 : j >= ref1; 151 | i = ref <= ref1 ? ++j : --j 152 | ) { 153 | currentChar = bindValue.charAt(i); 154 | if (currentChar === ")") { 155 | if (openParenCount === 0) { 156 | return i; 157 | } else { 158 | openParenCount--; 159 | } 160 | } else if (currentChar === "(") { 161 | openParenCount++; 162 | } 163 | } 164 | throw new Error("Unbalanced parenthesis"); 165 | } 166 | 167 | static elementMatch(el, selector) { 168 | return ( 169 | el.matches || 170 | el.matchesSelector || 171 | el.msMatchesSelector || 172 | el.mozMatchesSelector || 173 | el.webkitMatchesSelector || 174 | el.oMatchesSelector 175 | ).call(el, selector); 176 | } 177 | 178 | static reactStyle(str) { 179 | if (!~str.indexOf("-")) return str; 180 | let retVal = ""; 181 | for (let block of str.split("-")) { 182 | if (retVal) { 183 | retVal += block[0].toUpperCase() + block.substr(1); 184 | } else { 185 | retVal += block; 186 | } 187 | } 188 | return retVal; 189 | } 190 | 191 | static addStyles(obj, styles) { 192 | if (styles) { 193 | for (let style in styles) { 194 | obj[Helper.reactStyle(style)] = styles[style]; 195 | } 196 | } 197 | } 198 | } 199 | 200 | Helper.nextId = 1; 201 | Helper.stringRegex = /^(?:"(?:[^"]|\\")*[^\\]"|'(?:[^']|\\')*[^\\]')$/; 202 | Helper.tokens = _tokens; 203 | Helper.dotRegex = /(\D\.)|(\.\D)/; 204 | -------------------------------------------------------------------------------- /src/lzstring.js: -------------------------------------------------------------------------------- 1 | var LZString = (function() { 2 | function o(o, r) { 3 | if (!t[o]) { 4 | t[o] = {}; 5 | for (var n = 0; n < o.length; n++) t[o][o.charAt(n)] = n; 6 | } 7 | return t[o][r]; 8 | } 9 | var r = String.fromCharCode, 10 | n = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 11 | e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$", 12 | t = {}, 13 | i = { 14 | compressToBase64: function(o) { 15 | if (null == o) return ""; 16 | var r = i._compress(o, 6, function(o) { 17 | return n.charAt(o); 18 | }); 19 | switch (r.length % 4) { 20 | default: 21 | case 0: 22 | return r; 23 | case 1: 24 | return r + "==="; 25 | case 2: 26 | return r + "=="; 27 | case 3: 28 | return r + "="; 29 | } 30 | }, 31 | decompressFromBase64: function(r) { 32 | return null == r 33 | ? "" 34 | : "" == r 35 | ? null 36 | : i._decompress(r.length, 32, function(e) { 37 | return o(n, r.charAt(e)); 38 | }); 39 | }, 40 | compressToUTF16: function(o) { 41 | return null == o 42 | ? "" 43 | : i._compress(o, 15, function(o) { 44 | return r(o + 32); 45 | }) + " "; 46 | }, 47 | decompressFromUTF16: function(o) { 48 | return null == o 49 | ? "" 50 | : "" == o 51 | ? null 52 | : i._decompress(o.length, 16384, function(r) { 53 | return o.charCodeAt(r) - 32; 54 | }); 55 | }, 56 | compressToUint8Array: function(o) { 57 | for ( 58 | var r = i.compress(o), 59 | n = new Uint8Array(2 * r.length), 60 | e = 0, 61 | t = r.length; 62 | t > e; 63 | e++ 64 | ) { 65 | var s = r.charCodeAt(e); 66 | (n[2 * e] = s >>> 8), (n[2 * e + 1] = s % 256); 67 | } 68 | return n; 69 | }, 70 | decompressFromUint8Array: function(o) { 71 | if (null === o || void 0 === o) return i.decompress(o); 72 | for (var n = new Array(o.length / 2), e = 0, t = n.length; t > e; e++) 73 | n[e] = 256 * o[2 * e] + o[2 * e + 1]; 74 | var s = []; 75 | return ( 76 | n.forEach(function(o) { 77 | s.push(r(o)); 78 | }), 79 | i.decompress(s.join("")) 80 | ); 81 | }, 82 | compressToEncodedURIComponent: function(o) { 83 | return null == o 84 | ? "" 85 | : i._compress(o, 6, function(o) { 86 | return e.charAt(o); 87 | }); 88 | }, 89 | decompressFromEncodedURIComponent: function(r) { 90 | return null == r 91 | ? "" 92 | : "" == r 93 | ? null 94 | : ((r = r.replace(/ /g, "+")), 95 | i._decompress(r.length, 32, function(n) { 96 | return o(e, r.charAt(n)); 97 | })); 98 | }, 99 | compress: function(o) { 100 | return i._compress(o, 16, function(o) { 101 | return r(o); 102 | }); 103 | }, 104 | _compress: function(o, r, n) { 105 | if (null == o) return ""; 106 | var e, 107 | t, 108 | i, 109 | s = {}, 110 | p = {}, 111 | u = "", 112 | c = "", 113 | a = "", 114 | l = 2, 115 | f = 3, 116 | h = 2, 117 | d = [], 118 | m = 0, 119 | v = 0; 120 | for (i = 0; i < o.length; i += 1) 121 | if ( 122 | ((u = o.charAt(i)), 123 | Object.prototype.hasOwnProperty.call(s, u) || 124 | ((s[u] = f++), (p[u] = !0)), 125 | (c = a + u), 126 | Object.prototype.hasOwnProperty.call(s, c)) 127 | ) 128 | a = c; 129 | else { 130 | if (Object.prototype.hasOwnProperty.call(p, a)) { 131 | if (a.charCodeAt(0) < 256) { 132 | for (e = 0; h > e; e++) 133 | (m <<= 1), 134 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++; 135 | for (t = a.charCodeAt(0), e = 0; 8 > e; e++) 136 | (m = (m << 1) | (1 & t)), 137 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 138 | (t >>= 1); 139 | } else { 140 | for (t = 1, e = 0; h > e; e++) 141 | (m = (m << 1) | t), 142 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 143 | (t = 0); 144 | for (t = a.charCodeAt(0), e = 0; 16 > e; e++) 145 | (m = (m << 1) | (1 & t)), 146 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 147 | (t >>= 1); 148 | } 149 | l--, 0 == l && ((l = Math.pow(2, h)), h++), delete p[a]; 150 | } else 151 | for (t = s[a], e = 0; h > e; e++) 152 | (m = (m << 1) | (1 & t)), 153 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 154 | (t >>= 1); 155 | l--, 156 | 0 == l && ((l = Math.pow(2, h)), h++), 157 | (s[c] = f++), 158 | (a = String(u)); 159 | } 160 | if ("" !== a) { 161 | if (Object.prototype.hasOwnProperty.call(p, a)) { 162 | if (a.charCodeAt(0) < 256) { 163 | for (e = 0; h > e; e++) 164 | (m <<= 1), v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++; 165 | for (t = a.charCodeAt(0), e = 0; 8 > e; e++) 166 | (m = (m << 1) | (1 & t)), 167 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 168 | (t >>= 1); 169 | } else { 170 | for (t = 1, e = 0; h > e; e++) 171 | (m = (m << 1) | t), 172 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 173 | (t = 0); 174 | for (t = a.charCodeAt(0), e = 0; 16 > e; e++) 175 | (m = (m << 1) | (1 & t)), 176 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 177 | (t >>= 1); 178 | } 179 | l--, 0 == l && ((l = Math.pow(2, h)), h++), delete p[a]; 180 | } else 181 | for (t = s[a], e = 0; h > e; e++) 182 | (m = (m << 1) | (1 & t)), 183 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 184 | (t >>= 1); 185 | l--, 0 == l && ((l = Math.pow(2, h)), h++); 186 | } 187 | for (t = 2, e = 0; h > e; e++) 188 | (m = (m << 1) | (1 & t)), 189 | v == r - 1 ? ((v = 0), d.push(n(m)), (m = 0)) : v++, 190 | (t >>= 1); 191 | for (;;) { 192 | if (((m <<= 1), v == r - 1)) { 193 | d.push(n(m)); 194 | break; 195 | } 196 | v++; 197 | } 198 | return d.join(""); 199 | }, 200 | decompress: function(o) { 201 | return null == o 202 | ? "" 203 | : "" == o 204 | ? null 205 | : i._decompress(o.length, 32768, function(r) { 206 | return o.charCodeAt(r); 207 | }); 208 | }, 209 | _decompress: function(o, n, e) { 210 | var t, 211 | i, 212 | s, 213 | p, 214 | u, 215 | c, 216 | a, 217 | l, 218 | f = [], 219 | h = 4, 220 | d = 4, 221 | m = 3, 222 | v = "", 223 | w = [], 224 | A = { val: e(0), position: n, index: 1 }; 225 | for (i = 0; 3 > i; i += 1) f[i] = i; 226 | for (p = 0, c = Math.pow(2, 2), a = 1; a != c; ) 227 | (u = A.val & A.position), 228 | (A.position >>= 1), 229 | 0 == A.position && ((A.position = n), (A.val = e(A.index++))), 230 | (p |= (u > 0 ? 1 : 0) * a), 231 | (a <<= 1); 232 | switch ((t = p)) { 233 | case 0: 234 | for (p = 0, c = Math.pow(2, 8), a = 1; a != c; ) 235 | (u = A.val & A.position), 236 | (A.position >>= 1), 237 | 0 == A.position && ((A.position = n), (A.val = e(A.index++))), 238 | (p |= (u > 0 ? 1 : 0) * a), 239 | (a <<= 1); 240 | l = r(p); 241 | break; 242 | case 1: 243 | for (p = 0, c = Math.pow(2, 16), a = 1; a != c; ) 244 | (u = A.val & A.position), 245 | (A.position >>= 1), 246 | 0 == A.position && ((A.position = n), (A.val = e(A.index++))), 247 | (p |= (u > 0 ? 1 : 0) * a), 248 | (a <<= 1); 249 | l = r(p); 250 | break; 251 | case 2: 252 | return ""; 253 | } 254 | for (f[3] = l, s = l, w.push(l); ; ) { 255 | if (A.index > o) return ""; 256 | for (p = 0, c = Math.pow(2, m), a = 1; a != c; ) 257 | (u = A.val & A.position), 258 | (A.position >>= 1), 259 | 0 == A.position && ((A.position = n), (A.val = e(A.index++))), 260 | (p |= (u > 0 ? 1 : 0) * a), 261 | (a <<= 1); 262 | switch ((l = p)) { 263 | case 0: 264 | for (p = 0, c = Math.pow(2, 8), a = 1; a != c; ) 265 | (u = A.val & A.position), 266 | (A.position >>= 1), 267 | 0 == A.position && ((A.position = n), (A.val = e(A.index++))), 268 | (p |= (u > 0 ? 1 : 0) * a), 269 | (a <<= 1); 270 | (f[d++] = r(p)), (l = d - 1), h--; 271 | break; 272 | case 1: 273 | for (p = 0, c = Math.pow(2, 16), a = 1; a != c; ) 274 | (u = A.val & A.position), 275 | (A.position >>= 1), 276 | 0 == A.position && ((A.position = n), (A.val = e(A.index++))), 277 | (p |= (u > 0 ? 1 : 0) * a), 278 | (a <<= 1); 279 | (f[d++] = r(p)), (l = d - 1), h--; 280 | break; 281 | case 2: 282 | return w.join(""); 283 | } 284 | if ((0 == h && ((h = Math.pow(2, m)), m++), f[l])) v = f[l]; 285 | else { 286 | if (l !== d) return null; 287 | v = s + s.charAt(0); 288 | } 289 | w.push(v), 290 | (f[d++] = s + v.charAt(0)), 291 | h--, 292 | (s = v), 293 | 0 == h && ((h = Math.pow(2, m)), m++); 294 | } 295 | } 296 | }; 297 | return i; 298 | })(); 299 | "function" == typeof define && define.amd 300 | ? define(function() { 301 | return LZString; 302 | }) 303 | : "undefined" != typeof module && 304 | null != module && 305 | (module.exports = LZString); 306 | -------------------------------------------------------------------------------- /src/parseBind.js: -------------------------------------------------------------------------------- 1 | var _bindingToken, 2 | _divisionLookBehind, 3 | _keywordRegexLookBehind, 4 | _operators, 5 | everyThingElse, 6 | oneNotSpace, 7 | specials, 8 | stringDouble, 9 | stringRegexp, 10 | stringSingle; 11 | 12 | stringDouble = '"(?:[^"\\\\]|\\\\.)*"'; 13 | 14 | stringSingle = "'(?:[^'\\\\]|\\\\.)*'"; 15 | 16 | stringRegexp = "/(?:[^/\\\\]|\\\\.)*/w*"; 17 | 18 | specials = ",\"'{}()/:[\\]"; 19 | 20 | everyThingElse = "[^\\s:,/][^" + specials + "]*[^\\s" + specials + "]"; 21 | 22 | oneNotSpace = "[^\\s]"; 23 | 24 | _bindingToken = RegExp( 25 | stringDouble + 26 | "|" + 27 | stringSingle + 28 | "|" + 29 | stringRegexp + 30 | "|" + 31 | everyThingElse + 32 | "|" + 33 | oneNotSpace, 34 | "g" 35 | ); 36 | 37 | _divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/; 38 | 39 | _keywordRegexLookBehind = { 40 | in: 1, 41 | return: 1, 42 | typeof: 1 43 | }; 44 | 45 | _operators = "+-*/&|=><"; 46 | 47 | const parseBind = function(objectLiteralString) { 48 | var c, depth, i, key, match, result, str, tok, toks, v, values; 49 | str = objectLiteralString && objectLiteralString.trim(); 50 | if (str.charCodeAt(0) === 123) { 51 | str = str.slice(1, -1); 52 | } 53 | result = {}; 54 | toks = str.match(_bindingToken); 55 | depth = 0; 56 | key = void 0; 57 | values = void 0; 58 | if (toks) { 59 | toks.push(","); 60 | i = -1; 61 | tok = void 0; 62 | while ((tok = toks[++i])) { 63 | c = tok.charCodeAt(0); 64 | if (c === 44) { 65 | if (depth <= 0) { 66 | if (key) { 67 | if (!values) { 68 | result["unknown"] = key; 69 | } else { 70 | v = values.join(""); 71 | if (v.indexOf("{") === 0) { 72 | v = parseBind(v); 73 | } 74 | result[key] = v; 75 | } 76 | } 77 | key = values = depth = 0; 78 | continue; 79 | } 80 | } else if (c === 58) { 81 | if (!values) { 82 | continue; 83 | } 84 | } else if (c === 47 && i && tok.length > 1) { 85 | match = toks[i - 1].match(_divisionLookBehind); 86 | if (match && !_keywordRegexLookBehind[match[0]]) { 87 | str = str.substr(str.indexOf(tok) + 1); 88 | toks = str.match(_bindingToken); 89 | toks.push(","); 90 | i = -1; 91 | tok = "/"; 92 | } 93 | } else if (c === 40 || c === 123 || c === 91) { 94 | ++depth; 95 | } else if (c === 41 || c === 125 || c === 93) { 96 | --depth; 97 | } else if (!key && !values) { 98 | key = c === 34 || c === 39 ? tok.slice(1, -1) : tok; 99 | continue; 100 | } 101 | if (~_operators.indexOf(tok[0])) { 102 | tok = " " + tok; 103 | } 104 | if (~_operators.indexOf(tok[tok.length - 1])) { 105 | tok += " "; 106 | } 107 | if (values) { 108 | values.push(tok); 109 | } else { 110 | values = [tok]; 111 | } 112 | } 113 | } 114 | return result; 115 | }; 116 | 117 | export default parseBind; 118 | -------------------------------------------------------------------------------- /src/reactive-array.js: -------------------------------------------------------------------------------- 1 | var ReactiveArray, 2 | extend = function(child, parent) { 3 | for (var key in parent) { 4 | if (hasProp.call(parent, key)) child[key] = parent[key]; 5 | } 6 | function ctor() { 7 | this.constructor = child; 8 | } 9 | ctor.prototype = parent.prototype; 10 | child.prototype = new ctor(); 11 | child.__super__ = parent.prototype; 12 | return child; 13 | }, 14 | hasProp = {}.hasOwnProperty; 15 | 16 | ReactiveArray = (function(superClass) { 17 | var isArray; 18 | 19 | extend(ReactiveArray, superClass); 20 | 21 | isArray = function(obj) { 22 | return obj instanceof Array; 23 | }; 24 | 25 | function ReactiveArray(p1, p2) { 26 | var dep, item, j, len, pause; 27 | dep = null; 28 | pause = false; 29 | this.changed = function() { 30 | if (dep && !pause) { 31 | return dep.changed(); 32 | } 33 | }; 34 | this.depend = function() { 35 | return dep.depend(); 36 | }; 37 | if (isArray(p1)) { 38 | for (j = 0, len = p1.length; j < len; j++) { 39 | item = p1[j]; 40 | this.push(item); 41 | } 42 | dep = p2; 43 | } else { 44 | dep = p1; 45 | } 46 | this.pause = function() { 47 | return (pause = true); 48 | }; 49 | this.resume = function() { 50 | pause = false; 51 | return this.changed(); 52 | }; 53 | } 54 | 55 | ReactiveArray.prototype.array = function() { 56 | this.depend(); 57 | return Array.prototype.slice.call(this); 58 | }; 59 | 60 | ReactiveArray.prototype.list = function() { 61 | this.depend(); 62 | return this; 63 | }; 64 | 65 | ReactiveArray.prototype.depend = function() { 66 | this.depend(); 67 | return this; 68 | }; 69 | 70 | ReactiveArray.prototype.push = function() { 71 | var item; 72 | item = ReactiveArray.__super__.push.apply(this, arguments); 73 | this.changed(); 74 | return item; 75 | }; 76 | 77 | ReactiveArray.prototype.unshift = function() { 78 | var item; 79 | item = ReactiveArray.__super__.unshift.apply(this, arguments); 80 | this.changed(); 81 | return item; 82 | }; 83 | 84 | ReactiveArray.prototype.pop = function() { 85 | var item; 86 | item = ReactiveArray.__super__.pop.apply(this, arguments); 87 | this.changed(); 88 | return item; 89 | }; 90 | 91 | ReactiveArray.prototype.shift = function() { 92 | var item; 93 | item = ReactiveArray.__super__.shift.apply(this, arguments); 94 | this.changed(); 95 | return item; 96 | }; 97 | 98 | ReactiveArray.prototype.remove = function(valueOrPredicate) { 99 | var i, predicate, removedValues, underlyingArray, value; 100 | underlyingArray = this; 101 | removedValues = []; 102 | predicate = 103 | typeof valueOrPredicate === "function" 104 | ? valueOrPredicate 105 | : function(value) { 106 | return value === valueOrPredicate; 107 | }; 108 | i = 0; 109 | while (i < underlyingArray.length) { 110 | value = underlyingArray[i]; 111 | if (predicate(value)) { 112 | removedValues.push(value); 113 | underlyingArray.splice(i, 1); 114 | i--; 115 | } 116 | i++; 117 | } 118 | if (removedValues.length) { 119 | this.changed(); 120 | } 121 | return removedValues; 122 | }; 123 | 124 | ReactiveArray.prototype.clear = function() { 125 | while (this.length) { 126 | this.pop(); 127 | } 128 | this.changed(); 129 | return this; 130 | }; 131 | 132 | ReactiveArray.prototype.concat = function() { 133 | var a, j, len, ret; 134 | ret = this.array(); 135 | for (j = 0, len = arguments.length; j < len; j++) { 136 | a = arguments[j]; 137 | if (a instanceof ReactiveArray) { 138 | ret = ret.concat(a.array()); 139 | } else { 140 | ret = ret.concat(a); 141 | } 142 | } 143 | return new ReactiveArray(ret); 144 | }; 145 | 146 | ReactiveArray.prototype.indexOf = function() { 147 | this.depend(); 148 | return ReactiveArray.__super__.indexOf.apply(this, arguments); 149 | }; 150 | 151 | ReactiveArray.prototype.join = function() { 152 | this.depend(); 153 | return ReactiveArray.__super__.join.apply(this, arguments); 154 | }; 155 | 156 | ReactiveArray.prototype.lastIndexOf = function() { 157 | this.depend(); 158 | return ReactiveArray.__super__.lastIndexOf.apply(this, arguments); 159 | }; 160 | 161 | ReactiveArray.prototype.reverse = function() { 162 | ReactiveArray.__super__.reverse.apply(this, arguments); 163 | this.changed(); 164 | return this; 165 | }; 166 | 167 | ReactiveArray.prototype.sort = function() { 168 | ReactiveArray.__super__.sort.apply(this, arguments); 169 | this.changed(); 170 | return this; 171 | }; 172 | 173 | ReactiveArray.prototype.splice = function() { 174 | var ret; 175 | ret = ReactiveArray.__super__.splice.apply(this, arguments); 176 | this.changed(); 177 | return ret; 178 | }; 179 | 180 | return ReactiveArray; 181 | })(Array); 182 | 183 | export default ReactiveArray; 184 | -------------------------------------------------------------------------------- /src/tracker.js: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////// 2 | // Package docs at http://docs.meteor.com/#tracker // 3 | ///////////////////////////////////////////////////// 4 | 5 | /** 6 | * @namespace Tracker 7 | * @summary The namespace for Tracker-related methods. 8 | */ 9 | var Tracker = {}; 10 | 11 | // http://docs.meteor.com/#tracker_active 12 | 13 | /** 14 | * @summary True if there is a current computation, meaning that dependencies on reactive data sources will be tracked and potentially cause the current computation to be rerun. 15 | * @locus Client 16 | * @type {Boolean} 17 | */ 18 | Tracker.active = false; 19 | 20 | // http://docs.meteor.com/#tracker_currentcomputation 21 | 22 | /** 23 | * @summary The current computation, or `null` if there isn't one. The current computation is the [`Tracker.Computation`](#tracker_computation) object created by the innermost active call to `Tracker.autorun`, and it's the computation that gains dependencies when reactive data sources are accessed. 24 | * @locus Client 25 | * @type {Tracker.Computation} 26 | */ 27 | Tracker.currentComputation = null; 28 | 29 | // References to all computations created within the Tracker by id. 30 | // Keeping these references on an underscore property gives more control to 31 | // tooling and packages extending Tracker without increasing the API surface. 32 | // These can used to monkey-patch computations, their functions, use 33 | // computation ids for tracking, etc. 34 | Tracker._computations = {}; 35 | 36 | var setCurrentComputation = function(c) { 37 | Tracker.currentComputation = c; 38 | Tracker.active = !!c; 39 | }; 40 | 41 | var _debugFunc = function() { 42 | return function() { 43 | console.error.apply(console, arguments); 44 | }; 45 | }; 46 | 47 | var _maybeSuppressMoreLogs = function(messagesLength) {}; 48 | 49 | var _throwOrLog = function(from, e) { 50 | if (throwFirstError) { 51 | throw e; 52 | } else { 53 | var printArgs = ["Exception from Tracker " + from + " function:"]; 54 | if (e.stack && e.message && e.name) { 55 | var idx = e.stack.indexOf(e.message); 56 | if (idx < 0 || idx > e.name.length + 2) { 57 | // check for "Error: " 58 | // message is not part of the stack 59 | var message = e.name + ": " + e.message; 60 | printArgs.push(message); 61 | } 62 | } 63 | printArgs.push(e.stack); 64 | _maybeSuppressMoreLogs(printArgs.length); 65 | 66 | for (var i = 0; i < printArgs.length; i++) { 67 | _debugFunc()(printArgs[i]); 68 | } 69 | } 70 | }; 71 | 72 | // Takes a function `f`, and wraps it in a `Meteor._noYieldsAllowed` 73 | // block if we are running on the server. On the client, returns the 74 | // original function (since `Meteor._noYieldsAllowed` is a 75 | // no-op). This has the benefit of not adding an unnecessary stack 76 | // frame on the client. 77 | var withNoYieldsAllowed = function(f) { 78 | return f; 79 | }; 80 | 81 | var nextId = 1; 82 | // computations whose callbacks we should call at flush time 83 | var pendingComputations = []; 84 | // `true` if a Tracker.flush is scheduled, or if we are in Tracker.flush now 85 | var willFlush = false; 86 | // `true` if we are in Tracker.flush now 87 | var inFlush = false; 88 | // `true` if we are computing a computation now, either first time 89 | // or recompute. This matches Tracker.active unless we are inside 90 | // Tracker.nonreactive, which nullfies currentComputation even though 91 | // an enclosing computation may still be running. 92 | var inCompute = false; 93 | // `true` if the `_throwFirstError` option was passed in to the call 94 | // to Tracker.flush that we are in. When set, throw rather than log the 95 | // first error encountered while flushing. Before throwing the error, 96 | // finish flushing (from a finally block), logging any subsequent 97 | // errors. 98 | var throwFirstError = false; 99 | 100 | var afterFlushCallbacks = []; 101 | 102 | var requireFlush = function() { 103 | if (!willFlush) { 104 | setTimeout(Tracker._runFlush, 0); 105 | willFlush = true; 106 | } 107 | }; 108 | 109 | // Tracker.Computation constructor is visible but private 110 | // (throws an error if you try to call it) 111 | var constructingComputation = false; 112 | 113 | // 114 | // http://docs.meteor.com/#tracker_computation 115 | 116 | /** 117 | * @summary A Computation object represents code that is repeatedly rerun 118 | * in response to 119 | * reactive data changes. Computations don't have return values; they just 120 | * perform actions, such as rerendering a template on the screen. Computations 121 | * are created using Tracker.autorun. Use stop to prevent further rerunning of a 122 | * computation. 123 | * @instancename computation 124 | */ 125 | Tracker.Computation = function(f, parent, onError) { 126 | if (!constructingComputation) 127 | throw new Error( 128 | "Tracker.Computation constructor is private; use Tracker.autorun" 129 | ); 130 | constructingComputation = false; 131 | 132 | var self = this; 133 | 134 | // http://docs.meteor.com/#computation_stopped 135 | 136 | /** 137 | * @summary True if this computation has been stopped. 138 | * @locus Client 139 | * @memberOf Tracker.Computation 140 | * @instance 141 | * @name stopped 142 | */ 143 | self.stopped = false; 144 | 145 | // http://docs.meteor.com/#computation_invalidated 146 | 147 | /** 148 | * @summary True if this computation has been invalidated (and not yet rerun), or if it has been stopped. 149 | * @locus Client 150 | * @memberOf Tracker.Computation 151 | * @instance 152 | * @name invalidated 153 | * @type {Boolean} 154 | */ 155 | self.invalidated = false; 156 | 157 | // http://docs.meteor.com/#computation_firstrun 158 | 159 | /** 160 | * @summary True during the initial run of the computation at the time `Tracker.autorun` is called, and false on subsequent reruns and at other times. 161 | * @locus Client 162 | * @memberOf Tracker.Computation 163 | * @instance 164 | * @name firstRun 165 | * @type {Boolean} 166 | */ 167 | self.firstRun = true; 168 | 169 | self._id = nextId++; 170 | self._onInvalidateCallbacks = []; 171 | self._onStopCallbacks = []; 172 | // the plan is at some point to use the parent relation 173 | // to constrain the order that computations are processed 174 | self._parent = parent; 175 | self._func = f; 176 | self._onError = onError; 177 | self._recomputing = false; 178 | 179 | // Register the computation within the global Tracker. 180 | Tracker._computations[self._id] = self; 181 | 182 | var errored = true; 183 | try { 184 | self._compute(); 185 | errored = false; 186 | } finally { 187 | self.firstRun = false; 188 | if (errored) self.stop(); 189 | } 190 | }; 191 | 192 | // http://docs.meteor.com/#computation_oninvalidate 193 | 194 | /** 195 | * @summary Registers `callback` to run when this computation is next invalidated, or runs it immediately if the computation is already invalidated. The callback is run exactly once and not upon future invalidations unless `onInvalidate` is called again after the computation becomes valid again. 196 | * @locus Client 197 | * @param {Function} callback Function to be called on invalidation. Receives one argument, the computation that was invalidated. 198 | */ 199 | Tracker.Computation.prototype.onInvalidate = function(f) { 200 | var self = this; 201 | 202 | if (typeof f !== "function") 203 | throw new Error("onInvalidate requires a function"); 204 | 205 | if (self.invalidated) { 206 | Tracker.nonreactive(function() { 207 | withNoYieldsAllowed(f)(self); 208 | }); 209 | } else { 210 | self._onInvalidateCallbacks.push(f); 211 | } 212 | }; 213 | 214 | /** 215 | * @summary Registers `callback` to run when this computation is stopped, or runs it immediately if the computation is already stopped. The callback is run after any `onInvalidate` callbacks. 216 | * @locus Client 217 | * @param {Function} callback Function to be called on stop. Receives one argument, the computation that was stopped. 218 | */ 219 | Tracker.Computation.prototype.onStop = function(f) { 220 | var self = this; 221 | 222 | if (typeof f !== "function") throw new Error("onStop requires a function"); 223 | 224 | if (self.stopped) { 225 | Tracker.nonreactive(function() { 226 | withNoYieldsAllowed(f)(self); 227 | }); 228 | } else { 229 | self._onStopCallbacks.push(f); 230 | } 231 | }; 232 | 233 | // http://docs.meteor.com/#computation_invalidate 234 | 235 | /** 236 | * @summary Invalidates this computation so that it will be rerun. 237 | * @locus Client 238 | */ 239 | Tracker.Computation.prototype.invalidate = function() { 240 | var self = this; 241 | if (!self.invalidated) { 242 | // if we're currently in _recompute(), don't enqueue 243 | // ourselves, since we'll rerun immediately anyway. 244 | if (!self._recomputing && !self.stopped) { 245 | requireFlush(); 246 | pendingComputations.push(this); 247 | } 248 | 249 | self.invalidated = true; 250 | 251 | // callbacks can't add callbacks, because 252 | // self.invalidated === true. 253 | for (var i = 0, f; (f = self._onInvalidateCallbacks[i]); i++) { 254 | Tracker.nonreactive(function() { 255 | withNoYieldsAllowed(f)(self); 256 | }); 257 | } 258 | self._onInvalidateCallbacks = []; 259 | } 260 | }; 261 | 262 | // http://docs.meteor.com/#computation_stop 263 | 264 | /** 265 | * @summary Prevents this computation from rerunning. 266 | * @locus Client 267 | */ 268 | Tracker.Computation.prototype.stop = function() { 269 | var self = this; 270 | 271 | if (!self.stopped) { 272 | self.stopped = true; 273 | self.invalidate(); 274 | // Unregister from global Tracker. 275 | delete Tracker._computations[self._id]; 276 | for (var i = 0, f; (f = self._onStopCallbacks[i]); i++) { 277 | Tracker.nonreactive(function() { 278 | withNoYieldsAllowed(f)(self); 279 | }); 280 | } 281 | self._onStopCallbacks = []; 282 | } 283 | }; 284 | 285 | Tracker.Computation.prototype._compute = function() { 286 | var self = this; 287 | self.invalidated = false; 288 | 289 | var previous = Tracker.currentComputation; 290 | setCurrentComputation(self); 291 | var previousInCompute = inCompute; 292 | inCompute = true; 293 | try { 294 | withNoYieldsAllowed(self._func)(self); 295 | } finally { 296 | setCurrentComputation(previous); 297 | inCompute = previousInCompute; 298 | } 299 | }; 300 | 301 | Tracker.Computation.prototype._needsRecompute = function() { 302 | var self = this; 303 | return self.invalidated && !self.stopped; 304 | }; 305 | 306 | Tracker.Computation.prototype._recompute = function() { 307 | var self = this; 308 | 309 | self._recomputing = true; 310 | try { 311 | if (self._needsRecompute()) { 312 | try { 313 | self._compute(); 314 | } catch (e) { 315 | if (self._onError) { 316 | self._onError(e); 317 | } else { 318 | _throwOrLog("recompute", e); 319 | } 320 | } 321 | } 322 | } finally { 323 | self._recomputing = false; 324 | } 325 | }; 326 | 327 | // 328 | // http://docs.meteor.com/#tracker_dependency 329 | 330 | /** 331 | * @summary A Dependency represents an atomic unit of reactive data that a 332 | * computation might depend on. Reactive data sources such as Session or 333 | * Minimongo internally create different Dependency objects for different 334 | * pieces of data, each of which may be depended on by multiple computations. 335 | * When the data changes, the computations are invalidated. 336 | * @class 337 | * @instanceName dependency 338 | */ 339 | Tracker.Dependency = function() { 340 | this._dependentsById = {}; 341 | }; 342 | 343 | // http://docs.meteor.com/#dependency_depend 344 | // 345 | // Adds `computation` to this set if it is not already 346 | // present. Returns true if `computation` is a new member of the set. 347 | // If no argument, defaults to currentComputation, or does nothing 348 | // if there is no currentComputation. 349 | 350 | /** 351 | * @summary Declares that the current computation (or `fromComputation` if given) depends on `dependency`. The computation will be invalidated the next time `dependency` changes. 352 | 353 | If there is no current computation and `depend()` is called with no arguments, it does nothing and returns false. 354 | 355 | Returns true if the computation is a new dependent of `dependency` rather than an existing one. 356 | * @locus Client 357 | * @param {Tracker.Computation} [fromComputation] An optional computation declared to depend on `dependency` instead of the current computation. 358 | * @returns {Boolean} 359 | */ 360 | Tracker.Dependency.prototype.depend = function(computation) { 361 | if (!computation) { 362 | if (!Tracker.active) return false; 363 | 364 | computation = Tracker.currentComputation; 365 | } 366 | var self = this; 367 | var id = computation._id; 368 | if (!(id in self._dependentsById)) { 369 | self._dependentsById[id] = computation; 370 | computation.onInvalidate(function() { 371 | delete self._dependentsById[id]; 372 | }); 373 | return true; 374 | } 375 | return false; 376 | }; 377 | 378 | // http://docs.meteor.com/#dependency_changed 379 | 380 | /** 381 | * @summary Invalidate all dependent computations immediately and remove them as dependents. 382 | * @locus Client 383 | */ 384 | Tracker.Dependency.prototype.changed = function() { 385 | var self = this; 386 | for (var id in self._dependentsById) self._dependentsById[id].invalidate(); 387 | }; 388 | 389 | // http://docs.meteor.com/#dependency_hasdependents 390 | 391 | /** 392 | * @summary True if this Dependency has one or more dependent Computations, which would be invalidated if this Dependency were to change. 393 | * @locus Client 394 | * @returns {Boolean} 395 | */ 396 | Tracker.Dependency.prototype.hasDependents = function() { 397 | var self = this; 398 | for (var id in self._dependentsById) return true; 399 | return false; 400 | }; 401 | 402 | // http://docs.meteor.com/#tracker_flush 403 | 404 | /** 405 | * @summary Process all reactive updates immediately and ensure that all invalidated computations are rerun. 406 | * @locus Client 407 | */ 408 | Tracker.flush = function(options) { 409 | Tracker._runFlush({ 410 | finishSynchronously: true, 411 | throwFirstError: options && options._throwFirstError 412 | }); 413 | }; 414 | 415 | // Run all pending computations and afterFlush callbacks. If we were not called 416 | // directly via Tracker.flush, this may return before they're all done to allow 417 | // the event loop to run a little before continuing. 418 | Tracker._runFlush = function(options) { 419 | // XXX What part of the comment below is still true? (We no longer 420 | // have Spark) 421 | // 422 | // Nested flush could plausibly happen if, say, a flush causes 423 | // DOM mutation, which causes a "blur" event, which runs an 424 | // app event handler that calls Tracker.flush. At the moment 425 | // Spark blocks event handlers during DOM mutation anyway, 426 | // because the LiveRange tree isn't valid. And we don't have 427 | // any useful notion of a nested flush. 428 | // 429 | // https://app.asana.com/0/159908330244/385138233856 430 | if (inFlush) throw new Error("Can't call Tracker.flush while flushing"); 431 | 432 | if (inCompute) throw new Error("Can't flush inside Tracker.autorun"); 433 | 434 | options = options || {}; 435 | 436 | inFlush = true; 437 | willFlush = true; 438 | throwFirstError = !!options.throwFirstError; 439 | 440 | var recomputedCount = 0; 441 | var finishedTry = false; 442 | try { 443 | while (pendingComputations.length || afterFlushCallbacks.length) { 444 | // recompute all pending computations 445 | while (pendingComputations.length) { 446 | var comp = pendingComputations.shift(); 447 | comp._recompute(); 448 | if (comp._needsRecompute()) { 449 | pendingComputations.unshift(comp); 450 | } 451 | 452 | if (!options.finishSynchronously && ++recomputedCount > 1000) { 453 | finishedTry = true; 454 | return; 455 | } 456 | } 457 | 458 | if (afterFlushCallbacks.length) { 459 | // call one afterFlush callback, which may 460 | // invalidate more computations 461 | var func = afterFlushCallbacks.shift(); 462 | try { 463 | func(); 464 | } catch (e) { 465 | _throwOrLog("afterFlush", e); 466 | } 467 | } 468 | } 469 | finishedTry = true; 470 | } finally { 471 | if (!finishedTry) { 472 | // we're erroring due to throwFirstError being true. 473 | inFlush = false; // needed before calling `Tracker.flush()` again 474 | // finish flushing 475 | Tracker._runFlush({ 476 | finishSynchronously: options.finishSynchronously, 477 | throwFirstError: false 478 | }); 479 | } 480 | willFlush = false; 481 | inFlush = false; 482 | if (pendingComputations.length || afterFlushCallbacks.length) { 483 | // We're yielding because we ran a bunch of computations and we aren't 484 | // required to finish synchronously, so we'd like to give the event loop a 485 | // chance. We should flush again soon. 486 | if (options.finishSynchronously) { 487 | throw new Error("still have more to do?"); // shouldn't happen 488 | } 489 | setTimeout(requireFlush, 10); 490 | } 491 | } 492 | }; 493 | 494 | // http://docs.meteor.com/#tracker_autorun 495 | // 496 | // Run f(). Record its dependencies. Rerun it whenever the 497 | // dependencies change. 498 | // 499 | // Returns a new Computation, which is also passed to f. 500 | // 501 | // Links the computation to the current computation 502 | // so that it is stopped if the current computation is invalidated. 503 | 504 | /** 505 | * @callback Tracker.ComputationFunction 506 | * @param {Tracker.Computation} 507 | */ 508 | /** 509 | * @summary Run a function now and rerun it later whenever its dependencies 510 | * change. Returns a Computation object that can be used to stop or observe the 511 | * rerunning. 512 | * @locus Client 513 | * @param {Tracker.ComputationFunction} runFunc The function to run. It receives 514 | * one argument: the Computation object that will be returned. 515 | * @param {Object} [options] 516 | * @param {Function} options.onError Optional. The function to run when an error 517 | * happens in the Computation. The only argument it recieves is the Error 518 | * thrown. Defaults to the error being logged to the console. 519 | * @returns {Tracker.Computation} 520 | */ 521 | Tracker.autorun = function(f, options) { 522 | if (typeof f !== "function") 523 | throw new Error("Tracker.autorun requires a function argument"); 524 | 525 | options = options || {}; 526 | 527 | constructingComputation = true; 528 | var c = new Tracker.Computation( 529 | f, 530 | Tracker.currentComputation, 531 | options.onError 532 | ); 533 | 534 | if (Tracker.active) 535 | Tracker.onInvalidate(function() { 536 | c.stop(); 537 | }); 538 | 539 | return c; 540 | }; 541 | 542 | // http://docs.meteor.com/#tracker_nonreactive 543 | // 544 | // Run `f` with no current computation, returning the return value 545 | // of `f`. Used to turn off reactivity for the duration of `f`, 546 | // so that reactive data sources accessed by `f` will not result in any 547 | // computations being invalidated. 548 | 549 | /** 550 | * @summary Run a function without tracking dependencies. 551 | * @locus Client 552 | * @param {Function} func A function to call immediately. 553 | */ 554 | Tracker.nonreactive = function(f) { 555 | var previous = Tracker.currentComputation; 556 | setCurrentComputation(null); 557 | try { 558 | return f(); 559 | } finally { 560 | setCurrentComputation(previous); 561 | } 562 | }; 563 | 564 | // http://docs.meteor.com/#tracker_oninvalidate 565 | 566 | /** 567 | * @summary Registers a new [`onInvalidate`](#computation_oninvalidate) callback on the current computation (which must exist), to be called immediately when the current computation is invalidated or stopped. 568 | * @locus Client 569 | * @param {Function} callback A callback function that will be invoked as `func(c)`, where `c` is the computation on which the callback is registered. 570 | */ 571 | Tracker.onInvalidate = function(f) { 572 | if (!Tracker.active) 573 | throw new Error("Tracker.onInvalidate requires a currentComputation"); 574 | 575 | Tracker.currentComputation.onInvalidate(f); 576 | }; 577 | 578 | // http://docs.meteor.com/#tracker_afterflush 579 | 580 | /** 581 | * @summary Schedules a function to be called during the next flush, or later in the current flush if one is in progress, after all invalidated computations have been rerun. The function will be run once and not on subsequent flushes unless `afterFlush` is called again. 582 | * @locus Client 583 | * @param {Function} callback A function to call at flush time. 584 | */ 585 | Tracker.afterFlush = function(f) { 586 | afterFlushCallbacks.push(f); 587 | requireFlush(); 588 | }; 589 | 590 | export default Tracker; 591 | -------------------------------------------------------------------------------- /src/viewmodel-onUrl.js: -------------------------------------------------------------------------------- 1 | import LZString from "./lzstring"; 2 | 3 | var getSavedData, getUrl, parseUri, updateQueryString; 4 | 5 | if (typeof window !== "undefined" && window.history) { 6 | (function(history) { 7 | var pushState, replaceState; 8 | pushState = history.pushState; 9 | replaceState = history.replaceState; 10 | if (pushState) { 11 | history.pushState = function(state, title, url) { 12 | if (typeof history.onstatechange === "function") { 13 | history.onstatechange(state, title, url); 14 | } 15 | return pushState.apply(history, arguments); 16 | }; 17 | history.replaceState = function(state, title, url) { 18 | if (typeof history.onstatechange === "function") { 19 | history.onstatechange(state, title, url); 20 | } 21 | return replaceState.apply(history, arguments); 22 | }; 23 | } else { 24 | history.pushState = function() {}; 25 | history.replaceState = function() {}; 26 | } 27 | })(window.history); 28 | } 29 | 30 | parseUri = function(str) { 31 | var i, m, o, uri; 32 | o = parseUri.options; 33 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str); 34 | uri = {}; 35 | i = 14; 36 | while (i--) { 37 | uri[o.key[i]] = m[i] || ""; 38 | } 39 | uri[o.q.name] = {}; 40 | uri[o.key[12]].replace(o.q.parser, function($0, $1, $2) { 41 | if ($1) { 42 | uri[o.q.name][$1] = $2; 43 | } 44 | }); 45 | return uri; 46 | }; 47 | 48 | parseUri.options = { 49 | strictMode: false, 50 | key: [ 51 | "source", 52 | "protocol", 53 | "authority", 54 | "userInfo", 55 | "user", 56 | "password", 57 | "host", 58 | "port", 59 | "relative", 60 | "path", 61 | "directory", 62 | "file", 63 | "query", 64 | "anchor" 65 | ], 66 | q: { 67 | name: "queryKey", 68 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 69 | }, 70 | parser: { 71 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 72 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 73 | } 74 | }; 75 | 76 | getUrl = function(target) { 77 | if (target == null) { 78 | target = document.URL; 79 | } 80 | return parseUri(target); 81 | }; 82 | 83 | updateQueryString = function(key, value, url) { 84 | var hash, re, separator; 85 | if (!url) { 86 | url = window.location.href; 87 | } 88 | re = new RegExp("([?&])" + key + "=.*?(&|#|$)(.*)", "gi"); 89 | hash = void 0; 90 | if (re.test(url)) { 91 | if (typeof value !== "undefined" && value !== null) { 92 | return url.replace(re, "$1" + key + "=" + value + "$2$3"); 93 | } else { 94 | hash = url.split("#"); 95 | url = hash[0].replace(re, "$1$3").replace(/(&|\?)$/, ""); 96 | if (typeof hash[1] !== "undefined" && hash[1] !== null) { 97 | url += "#" + hash[1]; 98 | } 99 | return url; 100 | } 101 | } else { 102 | if (typeof value !== "undefined" && value !== null) { 103 | separator = url.indexOf("?") !== -1 ? "&" : "?"; 104 | hash = url.split("#"); 105 | url = hash[0] + separator + key + "=" + value; 106 | if (typeof hash[1] !== "undefined" && hash[1] !== null) { 107 | url += "#" + hash[1]; 108 | } 109 | return url; 110 | } else { 111 | return url; 112 | } 113 | } 114 | }; 115 | 116 | getSavedData = function(url) { 117 | var dataString, obj, urlData; 118 | if (url == null) { 119 | url = document.URL; 120 | } 121 | urlData = getUrl(url).queryKey.vmdata; 122 | if (!urlData) { 123 | return; 124 | } 125 | dataString = LZString.decompressFromEncodedURIComponent(urlData); 126 | obj = {}; 127 | try { 128 | return (obj = JSON.parse(dataString)); 129 | } finally { 130 | return obj; 131 | } 132 | }; 133 | 134 | var getSaveUrl = function(vmObject) { 135 | return function(viewmodel) { 136 | if (typeof window === "undefined") return; 137 | const vmHash = vmObject.getPathToRoot(viewmodel); 138 | viewmodel.vmComputations.push( 139 | vmObject.Tracker.autorun(function(c) { 140 | var data, dataCompressed, dataString, fields, savedData, url; 141 | 142 | url = window.location.href; 143 | savedData = getSavedData() || {}; 144 | fields = 145 | viewmodel.onUrl() instanceof Array 146 | ? viewmodel.onUrl() 147 | : [viewmodel.onUrl()]; 148 | data = viewmodel.data(fields); 149 | savedData[vmHash] = data; 150 | dataString = JSON.stringify(savedData); 151 | dataCompressed = LZString.compressToEncodedURIComponent(dataString); 152 | url = updateQueryString("vmdata", dataCompressed, url); 153 | if (!c.firstRun && document.URL !== url && window.history) { 154 | window.history.pushState(null, null, url); 155 | } 156 | }) 157 | ); 158 | }; 159 | }; 160 | 161 | var getLoadUrl = function(vmObject) { 162 | return function(viewmodel) { 163 | if (typeof window === "undefined") return; 164 | var updateFromUrl; 165 | updateFromUrl = function(state, title, url) { 166 | var data, savedData, vmHash; 167 | if (url == null) { 168 | url = document.URL; 169 | } 170 | data = getSavedData(url); 171 | if (!data) { 172 | return; 173 | } 174 | vmHash = vmObject.getPathToRoot(viewmodel); 175 | savedData = data[vmHash]; 176 | if (savedData) { 177 | return viewmodel.load(savedData); 178 | } 179 | }; 180 | window.onpopstate = window.history.onstatechange = updateFromUrl; 181 | updateFromUrl(); 182 | }; 183 | }; 184 | 185 | export { getSaveUrl, getLoadUrl }; 186 | -------------------------------------------------------------------------------- /src/viewmodel-property.js: -------------------------------------------------------------------------------- 1 | const ValueTypes = { 2 | string: 1, 3 | number: 2, 4 | integer: 3, 5 | boolean: 4, 6 | object: 5, 7 | date: 6, 8 | array: 7, 9 | any: 8 10 | }; 11 | 12 | const isNull = function(obj) { 13 | return obj === null; 14 | }; 15 | 16 | const isUndefined = function(obj) { 17 | return typeof obj === "undefined"; 18 | }; 19 | 20 | const isArray = function(obj) { 21 | return obj instanceof Array; 22 | }; 23 | 24 | const isNumber = function(obj) { 25 | // jQuery's isNumeric 26 | return !isArray(obj) && obj - parseFloat(obj) + 1 >= 0; 27 | }; 28 | 29 | const isInteger = function(n) { 30 | if (!isNumber(n) || ~n.toString().indexOf(".")) return false; 31 | 32 | var value = parseFloat(n); 33 | return value === +value && value === (value | 0); 34 | }; 35 | 36 | const isObject = function(obj) { 37 | return typeof obj === "object" && obj !== null && !(obj instanceof Date); 38 | }; 39 | 40 | const isString = function(str) { 41 | return typeof str === "string" || str instanceof String; 42 | }; 43 | 44 | const isBoolean = function(val) { 45 | return typeof val === "boolean"; 46 | }; 47 | 48 | const isDate = function(obj) { 49 | return obj instanceof Date; 50 | }; 51 | 52 | export default class Property { 53 | constructor() { 54 | this.checks = []; 55 | this.checksAsync = []; 56 | this.convertIns = []; 57 | this.convertOuts = []; 58 | this.beforeUpdates = []; 59 | this.afterUpdates = []; 60 | this.defaultValue = undefined; 61 | this.validMessageValue = ""; 62 | this.invalidMessageValue = ""; 63 | this.validatingMessageValue = ""; 64 | this.valueType = ValueTypes.any; 65 | } 66 | verify(value, context) { 67 | for (var check of this.checks) { 68 | if (!check.call(context, value)) return false; 69 | } 70 | return true; 71 | } 72 | verifyAsync(value, done, context) { 73 | for (var check of this.checksAsync) { 74 | check.call(context, value, done); 75 | } 76 | } 77 | hasAsync() { 78 | return this.checksAsync.length; 79 | } 80 | setDefault(value) { 81 | if (typeof this.defaultValue === "undefined") this.defaultValue = value; 82 | } 83 | 84 | convertIn(fun) { 85 | this.convertIns.push(fun); 86 | return this; 87 | } 88 | convertOut(fun) { 89 | this.convertOuts.push(fun); 90 | return this; 91 | } 92 | 93 | beforeUpdate(fun) { 94 | this.beforeUpdates.push(fun); 95 | return this; 96 | } 97 | afterUpdate(fun) { 98 | this.afterUpdates.push(fun); 99 | return this; 100 | } 101 | 102 | convertValueIn(value, context) { 103 | let final = value; 104 | for (var convert of this.convertIns) { 105 | final = convert.call(context, final); 106 | } 107 | return final; 108 | } 109 | 110 | convertValueOut(value, context) { 111 | let final = value; 112 | for (var convert of this.convertOuts) { 113 | final = convert.call(context, final); 114 | } 115 | return final; 116 | } 117 | 118 | beforeValueUpdate(value, context) { 119 | for (var fun of this.beforeUpdates) { 120 | fun.call(context, value); 121 | } 122 | } 123 | 124 | afterValueUpdate(value, context) { 125 | for (var fun of this.afterUpdates) { 126 | fun.call(context, value); 127 | } 128 | } 129 | 130 | convertedValue(value) { 131 | if (this.valueType === ValueTypes.integer) { 132 | return parseInt(value); 133 | } else if (this.valueType === ValueTypes.string) { 134 | return value.toString(); 135 | } else if (this.valueType === ValueTypes.number) { 136 | return parseFloat(value); 137 | } else if (this.valueType === ValueTypes.date) { 138 | return new Date(value); 139 | } else if (this.valueType === ValueTypes.boolean) { 140 | return !!value; 141 | } 142 | return value; 143 | } 144 | 145 | min(minValue) { 146 | this.checks.push(value => { 147 | const toMatch = this.convertedValue(value); 148 | if (this.valueType === ValueTypes.string) { 149 | return toMatch.length >= minValue; 150 | } else { 151 | return toMatch >= minValue; 152 | } 153 | }); 154 | return this; 155 | } 156 | 157 | max(maxValue) { 158 | this.checks.push(value => { 159 | const toMatch = this.convertedValue(value); 160 | if (this.valueType === ValueTypes.string) { 161 | return toMatch.length <= maxValue; 162 | } else { 163 | return toMatch <= maxValue; 164 | } 165 | }); 166 | return this; 167 | } 168 | 169 | equal(value) { 170 | this.checks.push(v => this.convertedValue(v) === value); 171 | return this; 172 | } 173 | notEqual(value) { 174 | this.checks.push(v => this.convertedValue(v) !== value); 175 | return this; 176 | } 177 | 178 | between(min, max) { 179 | this.checks.push(value => { 180 | const toMatch = this.convertedValue(value); 181 | if (this.valueType === ValueTypes.string) { 182 | return toMatch.length >= min && toMatch.length <= max; 183 | } else { 184 | return toMatch >= min && toMatch <= max; 185 | } 186 | }); 187 | return this; 188 | } 189 | notBetween(min, max) { 190 | this.checks.push(value => { 191 | const toMatch = this.convertedValue(value); 192 | if (this.valueType === ValueTypes.string) { 193 | return toMatch.length < min || toMatch.length > max; 194 | } else { 195 | return toMatch < min || toMatch > max; 196 | } 197 | }); 198 | return this; 199 | } 200 | 201 | regex(regexp) { 202 | this.checks.push(v => regexp.test(v)); 203 | return this; 204 | } 205 | 206 | validate(fun) { 207 | this.checks.push(fun); 208 | return this; 209 | } 210 | 211 | validateAsync(fun) { 212 | this.checksAsync.push(fun); 213 | return this; 214 | } 215 | 216 | default(value) { 217 | this.defaultValue = value; 218 | return this; 219 | } 220 | validMessage(message) { 221 | this.validMessageValue = message; 222 | return this; 223 | } 224 | invalidMessage(message) { 225 | this.invalidMessageValue = message; 226 | return this; 227 | } 228 | validatingMessage(message) { 229 | this.validatingMessageValue = message; 230 | return this; 231 | } 232 | 233 | get notBlank() { 234 | this.checks.push(value => isString(value) && !!value.trim().length); 235 | return this; 236 | } 237 | get string() { 238 | this.setDefault(""); 239 | this.valueType = ValueTypes.string; 240 | this.checks.push(value => isString(value)); 241 | return this; 242 | } 243 | get integer() { 244 | this.setDefault(0); 245 | this.valueType = ValueTypes.integer; 246 | this.checks.push(n => isInteger(n)); 247 | return this; 248 | } 249 | get number() { 250 | this.setDefault(0); 251 | this.valueType = ValueTypes.number; 252 | this.checks.push(value => isNumber(value)); 253 | return this; 254 | } 255 | get boolean() { 256 | this.setDefault(false); 257 | this.valueType = ValueTypes.boolean; 258 | this.checks.push(value => isBoolean(value)); 259 | return this; 260 | } 261 | get object() { 262 | this.setDefault({}); 263 | this.valueType = ValueTypes.object; 264 | this.checks.push(value => isObject(value)); 265 | return this; 266 | } 267 | get date() { 268 | this.setDefault(new Date()); 269 | this.valueType = ValueTypes.date; 270 | this.checks.push(value => value instanceof Date); 271 | return this; 272 | } 273 | get array() { 274 | this.setDefault([]); 275 | this.valueType = ValueTypes.array; 276 | this.checks.push(value => _.isArray(value)); 277 | return this; 278 | } 279 | get convert() { 280 | if (this.valueType === ValueTypes.integer) { 281 | this.convertIn(value => parseInt(value)); 282 | } else if (this.valueType === ValueTypes.string) { 283 | this.convertIn(value => value.toString()); 284 | } else if (this.valueType === ValueTypes.number) { 285 | this.convertIn(value => parseFloat(value)); 286 | } else if (this.valueType === ValueTypes.date) { 287 | this.convertIn(value => Date.parse(value)); 288 | } else if (this.valueType === ValueTypes.boolean) { 289 | this.convertIn(value => !!value); 290 | } 291 | return this; 292 | } 293 | 294 | static validator(value) { 295 | const property = new Property(); 296 | if (isString(value)) { 297 | return property.string; 298 | } else if (isNumber(value)) { 299 | return property.number; 300 | } else if (isDate(value)) { 301 | return property.date; 302 | } else if (isBoolean(value)) { 303 | return property.boolean; 304 | } else if (isObject(value)) { 305 | return property.object; 306 | } else if (isArray(value)) { 307 | return property.array; 308 | } else { 309 | return property; 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/viewmodel.js: -------------------------------------------------------------------------------- 1 | import Tracker from "./tracker"; 2 | import H from "./helper"; 3 | import ReactiveArray from "./reactive-array"; 4 | import Property from "./viewmodel-property"; 5 | import parseBind from "./parseBind"; 6 | import presetBindings from "./bindings"; 7 | import { getSaveUrl, getLoadUrl } from "./viewmodel-onUrl"; 8 | 9 | const pendingShared = []; 10 | let savedOnUrl = []; 11 | 12 | export default class ViewModel { 13 | static nextId() { 14 | return H.nextId++; 15 | } 16 | 17 | static global(obj) { 18 | ViewModel.globals.push(obj); 19 | } 20 | 21 | static prepareRoot() { 22 | if (!ViewModel.rootComponents) { 23 | const dep = new ViewModel.Tracker.Dependency(); 24 | ViewModel.rootComponents = new ReactiveArray(dep); 25 | } 26 | } 27 | 28 | static add(component) { 29 | const name = component.vmComponentName; 30 | if (!ViewModel.components[name]) { 31 | ViewModel.components[name] = {}; 32 | } 33 | ViewModel.components[name][component.vmId] = component; 34 | if (!component.parent()) { 35 | ViewModel.prepareRoot(); 36 | ViewModel.rootComponents.push(component); 37 | } 38 | } 39 | 40 | static find(nameOrPredicate, predicateOrNothing, onlyOne = false) { 41 | const name = H.isString(nameOrPredicate) && nameOrPredicate; 42 | const predicate = 43 | (H.isFunction(predicateOrNothing) && predicateOrNothing) || 44 | (H.isFunction(nameOrPredicate) && nameOrPredicate); 45 | let collection; 46 | if (name) { 47 | if (ViewModel.components[name]) 48 | collection = { all: ViewModel.components[name] }; 49 | } else { 50 | collection = ViewModel.components; 51 | } 52 | if (!collection) return []; 53 | const result = []; 54 | for (let groupName in collection) { 55 | let group = collection[groupName]; 56 | for (let item in group) { 57 | if (!predicate || predicate(group[item])) { 58 | result.push(group[item]); 59 | if (onlyOne) return result; 60 | } 61 | } 62 | } 63 | return result; 64 | } 65 | 66 | static findOne(nameOrPredicate, predicateOrNothing) { 67 | const results = ViewModel.find(nameOrPredicate, predicateOrNothing, true); 68 | if (results.length) { 69 | return results[0]; 70 | } 71 | } 72 | 73 | static mixin(obj) { 74 | for (let key in obj) { 75 | ViewModel.mixins[key] = obj[key]; 76 | } 77 | } 78 | 79 | static signal(obj) { 80 | for (let key in obj) { 81 | ViewModel.signals[key] = obj[key]; 82 | } 83 | } 84 | 85 | static share(obj) { 86 | pendingShared.push(obj); 87 | } 88 | static loadPendingShared() { 89 | if (pendingShared.length === 0) return; 90 | for (var obj of pendingShared) { 91 | for (let key in obj) { 92 | ViewModel.shared[key] = { vmChange() {} }; 93 | let value = obj[key]; 94 | for (let prop in value) { 95 | let content = value[prop]; 96 | if (H.isFunction(content) || ViewModel.properties[prop]) { 97 | ViewModel.shared[key][prop] = content; 98 | } else { 99 | const sharedProp = ViewModel.prop(content, ViewModel.shared[key]); 100 | sharedProp.vmSharedProp = true; 101 | ViewModel.shared[key][prop] = sharedProp; 102 | } 103 | } 104 | } 105 | } 106 | pendingShared.length = 0; 107 | } 108 | 109 | static prop(initial, component) { 110 | const dependency = new ViewModel.Tracker.Dependency(); 111 | const oldChanged = dependency.changed.bind(dependency); 112 | const components = {}; 113 | if (component && !components[component.vmId]) 114 | components[component.vmId] = component; 115 | dependency.changed = function() { 116 | for (let key in components) { 117 | let c = components[key]; 118 | c.vmChange(); 119 | } 120 | oldChanged(); 121 | }; 122 | 123 | const initialValue = 124 | initial instanceof ViewModel.Property ? initial.defaultValue : initial; 125 | let _value = undefined; 126 | const reset = function() { 127 | if (initialValue instanceof Array) { 128 | _value = new ReactiveArray(initialValue, dependency); 129 | } else { 130 | _value = initialValue; 131 | } 132 | }; 133 | 134 | reset(); 135 | 136 | const validator = 137 | initial instanceof ViewModel.Property 138 | ? initial 139 | : ViewModel.Property.validator(initial); 140 | 141 | const changeValue = function(value) { 142 | const prevValue = _value; 143 | if (validator.beforeUpdates.length) { 144 | validator.beforeValueUpdate(value, component); 145 | } 146 | 147 | if (value instanceof Array) { 148 | _value = new ReactiveArray(value, dependency); 149 | } else { 150 | _value = value; 151 | } 152 | 153 | if (validator.convertIns.length) { 154 | _value = validator.convertValueIn(_value, component); 155 | } 156 | 157 | if (validator.afterUpdates.length) { 158 | validator.afterValueUpdate(prevValue, component); 159 | } 160 | 161 | return dependency.changed(); 162 | }; 163 | 164 | const funProp = function(value) { 165 | if (arguments.length) { 166 | if (_value !== value) { 167 | if (funProp.delay > 0) { 168 | ViewModel.delay(funProp.delay, funProp.vmPropId, function() { 169 | changeValue(value); 170 | }); 171 | } else { 172 | changeValue(value); 173 | } 174 | } 175 | } else { 176 | dependency.depend(); 177 | } 178 | if (validator.convertOuts.length) { 179 | return validator.convertValueOut(_value, component); 180 | } else { 181 | return _value; 182 | } 183 | }; 184 | funProp.property = validator; 185 | funProp.reset = function() { 186 | reset(); 187 | dependency.changed(); 188 | }; 189 | funProp.depend = function() { 190 | dependency.depend(); 191 | }; 192 | funProp.changed = function() { 193 | dependency.changed(); 194 | }; 195 | funProp.delay = 0; 196 | funProp.vmPropId = ViewModel.nextId(); 197 | funProp.addComponent = function(component) { 198 | if (!components[component.vmId]) components[component.vmId] = component; 199 | }; 200 | Object.defineProperty(funProp, "value", { 201 | get() { 202 | return _value; 203 | } 204 | }); 205 | 206 | const hasAsync = validator.hasAsync(); 207 | let validationAsync = { 208 | hasResult: false, 209 | result: false, 210 | pending: false, 211 | value: undefined 212 | }; 213 | 214 | const getDone = hasAsync 215 | ? function(initialValue) { 216 | validationAsync = { 217 | hasResult: false, 218 | result: false, 219 | pending: true, 220 | value: initialValue 221 | }; 222 | return function(result) { 223 | validationAsync.hasResult = true; 224 | validationAsync.result = result; 225 | validationAsync.pending = false; 226 | ViewModel.Tracker.afterFlush(function() { 227 | return dependency.changed(); 228 | }); 229 | }; 230 | } 231 | : void 0; 232 | 233 | funProp.valid = function(noAsync) { 234 | dependency.depend(); 235 | if (noAsync && funProp.validating()) return false; 236 | const validSync = validator.verify(_value, component); 237 | if (!validSync || noAsync || !hasAsync) { 238 | if (!validSync) { 239 | return false; 240 | } else if ( 241 | hasAsync && 242 | validationAsync.hasResult && 243 | _value === validationAsync.value 244 | ) { 245 | return validationAsync.result; 246 | } else { 247 | return true; 248 | } 249 | } else { 250 | if (validationAsync.hasResult && _value === validationAsync.value) { 251 | return validationAsync.result; 252 | } else if (_value !== validationAsync.value) { 253 | validator.verifyAsync(_value, getDone(_value), component); 254 | return false; 255 | } 256 | } 257 | }; 258 | 259 | funProp.validMessage = function() { 260 | return validator.validMessageValue; 261 | }; 262 | 263 | funProp.invalid = function(noAsync) { 264 | return !this.valid(noAsync); 265 | }; 266 | 267 | funProp.invalidMessage = function() { 268 | return validator.invalidMessageValue; 269 | }; 270 | 271 | funProp.validatingMessage = function() { 272 | return validator.validatingMessageValue; 273 | }; 274 | 275 | funProp.validating = function() { 276 | if (!hasAsync) { 277 | return false; 278 | } 279 | dependency.depend(); 280 | return validationAsync.pending; 281 | }; 282 | 283 | funProp.message = function() { 284 | if (this.valid(true)) { 285 | return validator.validMessageValue; 286 | } else { 287 | return ( 288 | (funProp.validating() && validator.validatingMessageValue) || 289 | validator.invalidMessageValue 290 | ); 291 | } 292 | }; 293 | 294 | funProp.validator = validator; 295 | 296 | return funProp; 297 | } 298 | 299 | static getValueRef(container, prop) { 300 | return function(element) { 301 | container.vmComputations.push( 302 | ViewModel.Tracker.autorun(function() { 303 | let value = container[prop](); 304 | value = value == null ? "" : value; 305 | if (element && value != element.value) { 306 | element.value = value; 307 | } 308 | }) 309 | ); 310 | }; 311 | } 312 | 313 | static getValue( 314 | container, 315 | repeatObject, 316 | repeatIndex, 317 | bindValue, 318 | viewmodel, 319 | funPropReserved 320 | ) { 321 | let value; 322 | if (arguments.length < 5) viewmodel = container; 323 | bindValue = bindValue.trim(); 324 | const ref = H.firstToken(bindValue), 325 | token = ref[0], 326 | tokenIndex = ref[1]; 327 | if (~tokenIndex) { 328 | const left = () => 329 | ViewModel.getValue( 330 | container, 331 | repeatObject, 332 | repeatIndex, 333 | bindValue.substring(0, tokenIndex), 334 | viewmodel 335 | ); 336 | const right = () => 337 | ViewModel.getValue( 338 | container, 339 | repeatObject, 340 | repeatIndex, 341 | bindValue.substring(tokenIndex + token.length), 342 | viewmodel 343 | ); 344 | value = H.tokens[token.trim()](left, right); 345 | } else if (bindValue === "this") { 346 | value = viewmodel; 347 | } else if (bindValue === "repeatObject") { 348 | value = repeatObject; 349 | } else if (bindValue === "repeatIndex") { 350 | value = repeatIndex; 351 | } else if (H.isQuoted(bindValue)) { 352 | value = H.removeQuotes(bindValue); 353 | } else { 354 | const negate = bindValue.charAt(0) === "!"; 355 | if (negate) { 356 | bindValue = bindValue.substring(1); 357 | } 358 | let dotIndex = bindValue.search(H.dotRegex); 359 | if (~dotIndex && bindValue.charAt(dotIndex) !== ".") { 360 | dotIndex += 1; 361 | } 362 | const parenIndexStart = bindValue.indexOf("("); 363 | const parenIndexEnd = H.getMatchingParenIndex(bindValue, parenIndexStart); 364 | const breakOnFirstDot = 365 | ~dotIndex && 366 | (!~parenIndexStart || 367 | dotIndex < parenIndexStart || 368 | dotIndex === parenIndexEnd + 1); 369 | if (breakOnFirstDot) { 370 | const newBindValue = bindValue.substring(dotIndex + 1); 371 | const newBindValueCheck = newBindValue.endsWith("()") 372 | ? newBindValue.substr(0, newBindValue.length - 2) 373 | : newBindValue; 374 | const newContainer = ViewModel.getValue( 375 | container, 376 | repeatObject, 377 | repeatIndex, 378 | bindValue.substring(0, dotIndex), 379 | viewmodel, 380 | ViewModel.funPropReserved[newBindValueCheck] 381 | ); 382 | value = ViewModel.getValue( 383 | newContainer, 384 | repeatObject, 385 | repeatIndex, 386 | newBindValue, 387 | viewmodel 388 | ); 389 | } else { 390 | if (container == null) { 391 | value = undefined; 392 | } else { 393 | let name = bindValue; 394 | const args = []; 395 | if (~parenIndexStart) { 396 | const parsed = ViewModel.parseBind(bindValue); 397 | name = Object.keys(parsed)[0]; 398 | const second = parsed[name]; 399 | if (second.length > 2) { 400 | const ref1 = second.substr(1, second.length - 2).split(","); 401 | for (let j = 0, len = ref1.length; j < len; j++) { 402 | let arg = ref1[j].trim(); 403 | let newArg; 404 | if (arg === "this") { 405 | newArg = viewmodel; 406 | } else if (H.isQuoted(arg)) { 407 | newArg = H.removeQuotes(arg); 408 | } else { 409 | const neg = arg.charAt(0) === "!"; 410 | if (neg) { 411 | arg = arg.substring(1); 412 | } 413 | arg = ViewModel.getValue( 414 | viewmodel, 415 | repeatObject, 416 | repeatIndex, 417 | arg, 418 | viewmodel 419 | ); 420 | if (viewmodel && arg in viewmodel) { 421 | newArg = ViewModel.getValue( 422 | viewmodel, 423 | repeatObject, 424 | repeatIndex, 425 | arg, 426 | viewmodel 427 | ); 428 | } else { 429 | newArg = arg; 430 | } 431 | if (neg) { 432 | newArg = !newArg; 433 | } 434 | } 435 | args.push(newArg); 436 | } 437 | } 438 | } 439 | const primitive = H.isPrimitive(name); 440 | if (container.vmId && !primitive && !container[name]) { 441 | container[name] = ViewModel.prop("", viewmodel); 442 | } 443 | if ( 444 | !primitive && 445 | !( 446 | container != null && 447 | (container[name] != null || 448 | H.isObject(container) || 449 | H.isString(container)) 450 | ) 451 | ) { 452 | const errorMsg = 453 | "Can't access '" + name + "' of '" + container + "'."; 454 | console.error(errorMsg); 455 | } else if (primitive) { 456 | value = H.getPrimitive(name); 457 | } else if (!(H.isString(container) || name in container)) { 458 | return undefined; 459 | } else { 460 | if (!funPropReserved && H.isFunction(container[name])) { 461 | value = container[name].apply(container, args); 462 | } else { 463 | value = container[name]; 464 | } 465 | } 466 | } 467 | } 468 | if (negate) { 469 | value = !value; 470 | } 471 | } 472 | return value; 473 | } 474 | 475 | static getVmValueGetter(component, repeatObject, repeatIndex, bindValue) { 476 | return function(optBindValue = bindValue) { 477 | return ViewModel.getValue( 478 | component, 479 | repeatObject, 480 | repeatIndex, 481 | optBindValue.toString(), 482 | component 483 | ); 484 | }; 485 | } 486 | 487 | static getVmValueSetter(component, repeatObject, repeatIndex, bindValue) { 488 | if (!H.isString(bindValue)) { 489 | return function() {}; 490 | } 491 | if (~bindValue.indexOf(")", bindValue.length - 1)) { 492 | return function() { 493 | return ViewModel.getValue( 494 | component, 495 | repeatObject, 496 | repeatIndex, 497 | bindValue 498 | ); 499 | }; 500 | } else { 501 | return function(value) { 502 | ViewModel.setValueFull( 503 | value, 504 | repeatObject, 505 | repeatIndex, 506 | component, 507 | bindValue, 508 | component 509 | ); 510 | }; 511 | } 512 | } 513 | 514 | static setValueFull( 515 | value, 516 | repeatObject, 517 | repeatIndex, 518 | container, 519 | bindValue, 520 | viewmodel, 521 | prevContainer = {} 522 | ) { 523 | var i, newBindValue, newContainer; 524 | const ref = H.firstToken(bindValue), 525 | token = ref[0], 526 | tokenIndex = ref[1]; 527 | if (H.dotRegex.test(bindValue) || ~tokenIndex) { 528 | if (~tokenIndex) { 529 | ViewModel.getValue( 530 | container, 531 | repeatObject, 532 | repeatIndex, 533 | bindValue, 534 | viewmodel 535 | ); 536 | } else { 537 | i = bindValue.search(H.dotRegex); 538 | if (bindValue.charAt(i) !== ".") { 539 | i += 1; 540 | } 541 | newContainer = ViewModel.getValue( 542 | container, 543 | repeatObject, 544 | repeatIndex, 545 | bindValue.substring(0, i), 546 | viewmodel 547 | ); 548 | newBindValue = bindValue.substring(i + 1); 549 | const thisContainer = { container, prevContainer }; 550 | ViewModel.setValueFull( 551 | value, 552 | repeatObject, 553 | repeatIndex, 554 | newContainer, 555 | newBindValue, 556 | viewmodel, 557 | thisContainer 558 | ); 559 | } 560 | } else { 561 | if (H.isFunction(container[bindValue])) { 562 | container[bindValue](value); 563 | } else { 564 | container[bindValue] = value; 565 | let cont = prevContainer; 566 | while (cont && cont.container) { 567 | if (cont.container.vmId) { 568 | cont.container.vmChange(); 569 | break; 570 | } else { 571 | cont = cont.prevContainer; 572 | } 573 | } 574 | } 575 | } 576 | } 577 | 578 | static setValue(viewmodel, repeatObject, repeatIndex, bindValue) { 579 | if (!H.isString(bindValue)) { 580 | return function() {}; 581 | } 582 | if (~bindValue.indexOf(")", bindValue.length - 1)) { 583 | return function() { 584 | return ViewModel.getValue( 585 | viewmodel, 586 | repeatObject, 587 | repeatIndex, 588 | bindValue, 589 | viewmodel 590 | ); 591 | }; 592 | } else { 593 | return function(value) { 594 | return ViewModel.setValueFull( 595 | value, 596 | repeatObject, 597 | repeatIndex, 598 | viewmodel, 599 | bindValue, 600 | viewmodel 601 | ); 602 | }; 603 | } 604 | } 605 | 606 | static getClass( 607 | component, 608 | repeatObject, 609 | repeatIndex, 610 | initialClass, 611 | bindText 612 | ) { 613 | const cssClass = [initialClass]; 614 | if (bindText.trim()[0] === "{") { 615 | const cssObj = ViewModel.parseBind(bindText); 616 | for (let key in cssObj) { 617 | let value = cssObj[key]; 618 | if (ViewModel.getValue(component, repeatObject, repeatIndex, value)) { 619 | cssClass.push(key); 620 | } 621 | } 622 | } else { 623 | cssClass.push( 624 | ViewModel.getValue(component, repeatObject, repeatIndex, bindText) 625 | ); 626 | } 627 | return cssClass.join(" "); 628 | } 629 | 630 | static getDisabled( 631 | component, 632 | repeatObject, 633 | repeatIndex, 634 | isEnabled, 635 | bindText 636 | ) { 637 | const value = ViewModel.getValue( 638 | component, 639 | repeatObject, 640 | repeatIndex, 641 | bindText 642 | ); 643 | return !!(isEnabled ? !value : value); 644 | } 645 | 646 | static getStyle( 647 | component, 648 | repeatObject, 649 | repeatIndex, 650 | initialStyle, 651 | bindText 652 | ) { 653 | let initialStyles; 654 | if (!!initialStyle) { 655 | initialStyles = ViewModel.parseBind(initialStyle.split(";").join(",")); 656 | } 657 | 658 | let objectStyles; 659 | if (bindText.trim()[0] === "[") { 660 | objectStyles = {}; 661 | const itemsString = bindText.substr(1, bindText.length - 2); 662 | const items = itemsString.split(","); 663 | for (let item of items) { 664 | const vmValue = ViewModel.getValue( 665 | component, 666 | repeatObject, 667 | repeatIndex, 668 | item 669 | ); 670 | let bag = H.isString(vmValue) ? ViewModel.parseBind(vmValue) : vmValue; 671 | for (let key in bag) { 672 | const value = bag[key]; 673 | objectStyles[key] = value; 674 | } 675 | } 676 | } else if (bindText.trim()[0] === "{") { 677 | objectStyles = {}; 678 | const preObjectStyles = ViewModel.parseBind(bindText); 679 | for (let key in preObjectStyles) { 680 | let value = preObjectStyles[key]; 681 | objectStyles[key] = ViewModel.getValue( 682 | component, 683 | repeatObject, 684 | repeatIndex, 685 | value 686 | ); 687 | } 688 | } else { 689 | const vmValue = ViewModel.getValue( 690 | component, 691 | repeatObject, 692 | repeatIndex, 693 | bindText 694 | ); 695 | if (H.isString(vmValue)) { 696 | const newValue = vmValue.split(";").join(","); 697 | objectStyles = ViewModel.parseBind(newValue); 698 | } else { 699 | objectStyles = vmValue; 700 | } 701 | } 702 | 703 | const styles = {}; 704 | H.addStyles(styles, initialStyles); 705 | H.addStyles(styles, objectStyles); 706 | return styles; 707 | } 708 | 709 | static parseBind(str) { 710 | return parseBind(str); 711 | } 712 | 713 | static loadIntoContainer(toLoad, container, component = container) { 714 | const loadObj = function(obj) { 715 | for (let key in obj) { 716 | const value = obj[key]; 717 | if (!(ViewModel.properties[key] || ViewModel.reserved[key])) { 718 | if (H.isFunction(value)) { 719 | container[key] = value; 720 | if (value.vmPropId) { 721 | container[key].addComponent(component); 722 | } 723 | } else if ( 724 | container[key] && 725 | container[key].vmPropId && 726 | H.isFunction(container[key]) 727 | ) { 728 | if (!container[key].vmSharedProp) { 729 | container[key](value); 730 | } 731 | } else { 732 | container[key] = ViewModel.prop(value, container); 733 | } 734 | } 735 | } 736 | }; 737 | if (toLoad instanceof Array) { 738 | for (let i = 0, len = toLoad.length; i < len; i++) { 739 | loadObj(toLoad[i]); 740 | } 741 | } else { 742 | loadObj(toLoad); 743 | } 744 | } 745 | 746 | // Special thanks to @dino and @faceyspacey for this implementation 747 | // shamelessly stolen from their TrackerReact project 748 | static autorunOnce(renderFunc, component) { 749 | const name = "vmRenderComputation"; 750 | let retValue; 751 | // Stop it just in case the autorun never re-ran 752 | if (component[name] && !component[name].stopped) component[name].stop(); 753 | 754 | component[name] = ViewModel.Tracker.nonreactive(() => { 755 | return ViewModel.Tracker.autorun(c => { 756 | if (c.firstRun) { 757 | retValue = renderFunc.call(component); 758 | } else { 759 | // Stop autorun here so rendering "phase" doesn't have extra work of also stopping autoruns; likely not too 760 | // important though. 761 | if (component[name]) component[name].stop(); 762 | component.vmChange(); 763 | } 764 | }); 765 | }); 766 | return retValue; 767 | } 768 | 769 | static prepareComponentWillMount(component) { 770 | const old = component.componentWillMount; 771 | component.componentWillMount = function() { 772 | let parent = this.props["data-vm-parent"]; 773 | if (parent && parent.children) { 774 | parent.children().push(this); 775 | } 776 | this.parent = function() { 777 | parent = this.props["data-vm-parent"]; 778 | if (parent && parent.vmId) { 779 | this.vmDependsOnParent = true; 780 | return parent; 781 | } else { 782 | return undefined; 783 | } 784 | }; 785 | this.load(this.props); 786 | 787 | const bind = this.props["data-bind"]; 788 | if (bind) { 789 | var bindObject = parseBind(bind); 790 | if (bindObject.ref) { 791 | this.parent()[bindObject.ref] = this; 792 | } 793 | } 794 | 795 | for (let fun of component.vmCreated) { 796 | fun.call(component); 797 | } 798 | 799 | let oldRender = this.render; 800 | this.render = () => ViewModel.autorunOnce(oldRender, this); 801 | if (old) old.call(component); 802 | }; 803 | } 804 | 805 | static prepareComponentDidMount(component) { 806 | const old = component.componentDidMount; 807 | const componentDidMount = function() { 808 | component.vmMounted = true; 809 | 810 | for (let fun of component.vmRendered) { 811 | setTimeout(() => fun.call(component)); 812 | } 813 | 814 | for (let autorun of component.vmAutorun) { 815 | component.vmComputations.push( 816 | ViewModel.Tracker.autorun(function(c) { 817 | autorun.call(component, c); 818 | }) 819 | ); 820 | } 821 | 822 | if (old) old.call(component); 823 | 824 | component.vmPathToRoot = () => ViewModel.getPathToRoot(component); 825 | 826 | if (component.onUrl) { 827 | const saveOnUrl = function(component) { 828 | return function() { 829 | ViewModel.loadUrl(component); 830 | ViewModel.saveUrl(component); 831 | }; 832 | }; 833 | const toSave = saveOnUrl(component); 834 | if (savedOnUrl) { 835 | savedOnUrl.push(toSave); 836 | } else { 837 | toSave(); 838 | } 839 | } 840 | 841 | if (savedOnUrl && !component.parent()) { 842 | savedOnUrl.forEach(function(fun) { 843 | fun(); 844 | }); 845 | savedOnUrl = null; 846 | } 847 | 848 | ViewModel.add(component); 849 | component.vmChanged = false; 850 | }; 851 | 852 | component.componentDidMount = componentDidMount; 853 | } 854 | 855 | static prepareComponentWillUnmount(component) { 856 | const old = component.componentWillUnmount; 857 | component.componentWillUnmount = function() { 858 | for (let fun of component.vmDestroyed) { 859 | fun.call(component); 860 | } 861 | this.vmComputations.forEach(c => c.stop()); 862 | this.vmRenderComputation.stop(); 863 | delete ViewModel.components[component.vmComponentName][component.vmId]; 864 | if (!component.parent()) { 865 | for (var i = ViewModel.rootComponents.length - 1; i >= 0; i--) { 866 | if (ViewModel.rootComponents[i].vmId === component.vmId) { 867 | ViewModel.rootComponents.splice(i, 1); 868 | break; 869 | } 870 | } 871 | } 872 | component.vmReferences = undefined; 873 | if (old) old.call(component); 874 | component.vmMounted = false; 875 | }; 876 | } 877 | 878 | static prepareComponentDidUpdate(component) { 879 | const old = component.componentDidUpdate; 880 | component.componentDidUpdate = function() { 881 | component.vmChanged = false; 882 | if (old) old.call(component); 883 | }; 884 | } 885 | 886 | static prepareShouldComponentUpdate(component) { 887 | if (!component.shouldComponentUpdate) { 888 | component.shouldComponentUpdate = function() { 889 | const parent = component.parent(); 890 | if ( 891 | component.vmChanged || 892 | (component.vmDependsOnParent && parent.vmChanged) 893 | ) { 894 | if ( 895 | parent && 896 | !parent.vmChanged && 897 | !component.hasOwnProperty("vmUpdateParent") 898 | ) { 899 | for (let ref in parent) { 900 | if (parent[ref] === component) { 901 | component.vmUpdateParent = true; 902 | break; 903 | } 904 | } 905 | if (!component.vmUpdateParent) { 906 | // Create the property in the component 907 | component.vmUpdateParent = false; 908 | } 909 | } 910 | if (component.vmUpdateParent) { 911 | parent.vmChange(); 912 | } 913 | return true; 914 | } 915 | 916 | return false; 917 | }; 918 | } 919 | } 920 | 921 | static prepareComponentWillReceiveProps(component) { 922 | const old = component.componentWillReceiveProps; 923 | component.componentWillReceiveProps = function(props) { 924 | this.load(props); 925 | if (old) old.call(component); 926 | }; 927 | } 928 | 929 | static prepareChildren(component) { 930 | const dependency = new ViewModel.Tracker.Dependency(); 931 | const oldChanged = dependency.changed.bind(dependency); 932 | dependency.changed = function() { 933 | component.vmChange(); 934 | oldChanged(); 935 | }; 936 | const array = new ReactiveArray([], dependency); 937 | const funProp = function(search) { 938 | array.depend(); 939 | if (arguments.length) { 940 | const predicate = H.isString(search) 941 | ? function(vm) { 942 | return vm.vmComponentName === search; 943 | } 944 | : search; 945 | return array.filter(predicate); 946 | } else { 947 | return array; 948 | } 949 | }; 950 | component.children = funProp; 951 | } 952 | 953 | static prepareData(component) { 954 | component.data = function(fields = []) { 955 | const js = {}; 956 | for (let prop in component) { 957 | if ( 958 | component[prop] && 959 | component[prop].vmPropId && 960 | (fields.length === 0 || ~fields.indexOf(prop)) 961 | ) { 962 | component[prop].depend(); 963 | let value = component[prop].value; 964 | if (value instanceof Array) { 965 | js[prop] = value.array(); 966 | } else { 967 | js[prop] = value; 968 | } 969 | } 970 | } 971 | return js; 972 | }; 973 | } 974 | 975 | static prepareValidations(component) { 976 | component.valid = function(fields = []) { 977 | for (let prop in component) { 978 | if ( 979 | component[prop] && 980 | component[prop].vmPropId && 981 | (fields.length === 0 || ~fields.indexOf(prop)) 982 | ) { 983 | if (!component[prop].valid(true)) { 984 | return false; 985 | } 986 | } 987 | } 988 | return true; 989 | }; 990 | 991 | component.validMessages = function(fields = []) { 992 | const messages = []; 993 | for (let prop in component) { 994 | if ( 995 | component[prop] && 996 | component[prop].vmPropId && 997 | (fields.length === 0 || ~fields.indexOf(prop)) 998 | ) { 999 | if (component[prop].valid(true)) { 1000 | let message = component[prop].validator.validMessageValue; 1001 | if (message) { 1002 | messages.push(message); 1003 | } 1004 | } 1005 | } 1006 | } 1007 | return messages; 1008 | }; 1009 | 1010 | component.invalid = function(fields = []) { 1011 | return !component.valid(fields); 1012 | }; 1013 | 1014 | component.invalidMessages = function(fields = []) { 1015 | const messages = []; 1016 | for (let prop in component) { 1017 | if ( 1018 | component[prop] && 1019 | component[prop].vmPropId && 1020 | (fields.length === 0 || ~fields.indexOf(prop)) 1021 | ) { 1022 | if (!component[prop].valid(true)) { 1023 | let message = 1024 | (component[prop].validating() && 1025 | component[prop].validator.validatingMessageValue) || 1026 | component[prop].validator.invalidMessageValue; 1027 | if (message) { 1028 | messages.push(message); 1029 | } 1030 | } 1031 | } 1032 | } 1033 | return messages; 1034 | }; 1035 | } 1036 | 1037 | static prepareReset(component) { 1038 | component.reset = function() { 1039 | for (let prop in component) { 1040 | if (component[prop] && component[prop].vmPropId) { 1041 | component[prop].reset(); 1042 | } 1043 | } 1044 | }; 1045 | } 1046 | 1047 | static loadMixinShare(toLoad, collection, component, bag) { 1048 | if (!toLoad) return; 1049 | if (toLoad instanceof Array) { 1050 | for (let element of toLoad) { 1051 | if (H.isString(element)) { 1052 | component.load(collection[element]); 1053 | bag[element] = null; 1054 | } else { 1055 | ViewModel.loadMixinShare(element, collection, component, bag); 1056 | } 1057 | } 1058 | } else if (H.isString(toLoad)) { 1059 | component.load(collection[toLoad]); 1060 | bag[toLoad] = null; 1061 | } else { 1062 | for (let ref in toLoad) { 1063 | const container = { vmChange: component.vmChange }; 1064 | const mixshare = toLoad[ref]; 1065 | if (mixshare instanceof Array) { 1066 | for (let item of mixshare) { 1067 | ViewModel.loadIntoContainer(collection[item], container, component); 1068 | bag[item] = ref; 1069 | } 1070 | } else { 1071 | ViewModel.loadIntoContainer( 1072 | collection[mixshare], 1073 | container, 1074 | component 1075 | ); 1076 | bag[mixshare] = ref; 1077 | } 1078 | component[ref] = container; 1079 | } 1080 | } 1081 | } 1082 | 1083 | static prepareLoad(component) { 1084 | component.load = function(toLoad) { 1085 | if (!toLoad) return; 1086 | 1087 | // Signals 1088 | for (let signal of ViewModel.signalsToLoad(toLoad.signal, component)) { 1089 | component.load(signal); 1090 | component.vmCreated.push(signal.onCreated); 1091 | component.vmDestroyed.push(signal.onDestroyed); 1092 | } 1093 | 1094 | // Shared 1095 | ViewModel.loadPendingShared(); 1096 | ViewModel.loadMixinShare( 1097 | toLoad.share, 1098 | ViewModel.shared, 1099 | component, 1100 | component.vmShares 1101 | ); 1102 | 1103 | // Mixins 1104 | ViewModel.loadMixinShare( 1105 | toLoad.mixin, 1106 | ViewModel.mixins, 1107 | component, 1108 | component.vmMixins 1109 | ); 1110 | 1111 | // Whatever is in 'load' is loaded before direct properties 1112 | component.load(toLoad.load); 1113 | 1114 | // Load the object into the component 1115 | // (direct properties) 1116 | ViewModel.loadIntoContainer(toLoad, component); 1117 | 1118 | const hooks = { 1119 | created: "vmCreated", 1120 | rendered: "vmRendered", 1121 | destroyed: "vmDestroyed", 1122 | autorun: "vmAutorun" 1123 | }; 1124 | 1125 | for (let hook in hooks) { 1126 | if (!toLoad[hook]) continue; 1127 | let vmProp = hooks[hook]; 1128 | if (toLoad[hook] instanceof Array) { 1129 | for (let item of toLoad[hook]) { 1130 | component[vmProp].push(item); 1131 | } 1132 | } else { 1133 | component[vmProp].push(toLoad[hook]); 1134 | } 1135 | } 1136 | }; 1137 | } 1138 | 1139 | static prepareComponent(componentName, component, initial) { 1140 | component.vmId = ViewModel.nextId(); 1141 | component.vmComponentName = componentName; 1142 | component.vmComputations = []; 1143 | component.vmCreated = []; 1144 | component.vmRendered = []; 1145 | component.vmDestroyed = []; 1146 | component.vmAutorun = []; 1147 | component.vmMixins = {}; 1148 | component.vmShares = {}; 1149 | component.vmSignals = {}; 1150 | const getHasComposition = function(bag) { 1151 | return function(name, prop) { 1152 | return bag.hasOwnProperty(name) && (!bag[name] || bag[name] === prop); 1153 | }; 1154 | }; 1155 | component.hasMixin = getHasComposition(component.vmMixins); 1156 | component.hasShare = getHasComposition(component.vmShares); 1157 | component.hasSignal = getHasComposition(component.vmSignals); 1158 | 1159 | component.vmChange = function() { 1160 | if (!component.vmChanged) { 1161 | component.vmChanged = true; 1162 | if (component.vmMounted) { 1163 | component.setState({}); 1164 | } 1165 | } 1166 | }; 1167 | component.vmReferences = {}; 1168 | 1169 | ViewModel.prepareLoad(component); 1170 | for (let global of ViewModel.globals) { 1171 | component.load(global); 1172 | } 1173 | component.load(initial); 1174 | component.child = filter => component.children(filter)[0]; 1175 | ViewModel.prepareChildren(component); 1176 | ViewModel.prepareComponentWillMount(component); 1177 | ViewModel.prepareComponentDidMount(component); 1178 | ViewModel.prepareComponentDidUpdate(component); 1179 | ViewModel.prepareComponentWillUnmount(component); 1180 | ViewModel.prepareShouldComponentUpdate(component); 1181 | ViewModel.prepareComponentWillReceiveProps(component); 1182 | ViewModel.prepareValidations(component); 1183 | ViewModel.prepareData(component); 1184 | ViewModel.prepareReset(component); 1185 | } 1186 | 1187 | static addBinding(binding) { 1188 | if (!binding.priority) binding.priority = 1; 1189 | if (binding.selector) binding.priority += 1; 1190 | if (binding.bindIf) binding.priority += 1; 1191 | if (!ViewModel.bindings[binding.name]) { 1192 | ViewModel.bindings[binding.name] = []; 1193 | } 1194 | ViewModel.bindings[binding.name].push(binding); 1195 | } 1196 | 1197 | static bindElement(component, repeatObject, repeatIndex, bindingText) { 1198 | return function(element) { 1199 | if (!element || element.vmBound) return; 1200 | element.vmBound = true; 1201 | 1202 | const bindId = ViewModel.nextId(); 1203 | const bindObject = ViewModel.parseBind(bindingText); 1204 | for (let bindName in bindObject) { 1205 | if (ViewModel.compiledBindings[bindName]) continue; 1206 | let bindValue = bindObject[bindName]; 1207 | if (~bindName.indexOf(" ")) { 1208 | for (let bindNameSingle of bindName.split(" ")) { 1209 | ViewModel.bindSingle( 1210 | component, 1211 | repeatObject, 1212 | repeatIndex, 1213 | bindObject, 1214 | element, 1215 | bindNameSingle, 1216 | bindId 1217 | ); 1218 | } 1219 | } else { 1220 | ViewModel.bindSingle( 1221 | component, 1222 | repeatObject, 1223 | repeatIndex, 1224 | bindObject, 1225 | element, 1226 | bindName, 1227 | bindId 1228 | ); 1229 | } 1230 | } 1231 | }; 1232 | } 1233 | 1234 | static bindSingle( 1235 | component, 1236 | repeatObject, 1237 | repeatIndex, 1238 | bindObject, 1239 | element, 1240 | bindName, 1241 | bindId 1242 | ) { 1243 | const bindArg = ViewModel.getBindArgument( 1244 | component, 1245 | repeatObject, 1246 | repeatIndex, 1247 | bindObject, 1248 | element, 1249 | bindName, 1250 | bindId 1251 | ); 1252 | const binding = ViewModel.getBinding(bindName, bindArg); 1253 | if (!binding) return; 1254 | 1255 | if (binding.bind) { 1256 | binding.bind(bindArg); 1257 | } 1258 | if (binding.autorun) { 1259 | bindArg.autorun(binding.autorun); 1260 | } 1261 | 1262 | if (binding.events) { 1263 | let func = function(eventName, eventFunc) { 1264 | const eventListener = function(event) { 1265 | eventFunc(bindArg, event); 1266 | }; 1267 | 1268 | bindArg.element.addEventListener(eventName, eventListener); 1269 | bindArg.component.vmDestroyed.push(() => { 1270 | bindArg.element.removeEventListener(eventName, eventListener); 1271 | }); 1272 | }; 1273 | for (let eventName in binding.events) { 1274 | let eventFunc = binding.events[eventName]; 1275 | if (~eventName.indexOf(" ")) { 1276 | for (let event of eventName.split(" ")) { 1277 | func(event, eventFunc); 1278 | } 1279 | } else { 1280 | func(eventName, eventFunc); 1281 | } 1282 | } 1283 | } 1284 | } 1285 | 1286 | static getPathToRoot(component) { 1287 | let parent; 1288 | if (component.parent && (parent = component.parent())) { 1289 | const children = parent.children(component.vmComponentName); 1290 | const index = children.indexOf(component); 1291 | return ( 1292 | ViewModel.getPathToRoot(parent) + 1293 | `[${index}]/${component.vmComponentName}/` 1294 | ); 1295 | } else { 1296 | return `${component.vmComponentName}/`; 1297 | } 1298 | } 1299 | 1300 | static getBinding(bindName, bindArg) { 1301 | let binding = null; 1302 | let bindingArray = ViewModel.bindings[bindName]; 1303 | if (bindingArray) { 1304 | if ( 1305 | bindingArray.length === 1 && 1306 | !(bindingArray[0].bindIf || bindingArray[0].selector) 1307 | ) { 1308 | binding = bindingArray[0]; 1309 | } else { 1310 | binding = bindingArray 1311 | .sort(function(a, b) { 1312 | b.priority - a.priority; 1313 | }) 1314 | .find(function(b) { 1315 | return !( 1316 | (b.bindIf && !b.bindIf(bindArg)) || 1317 | (b.selector && !H.elementMatch(bindArg.element, b.selector)) 1318 | ); 1319 | }); 1320 | } 1321 | } 1322 | return binding || ViewModel.getBinding("default", bindArg); 1323 | } 1324 | 1325 | static getBindArgument( 1326 | component, 1327 | repeatObject, 1328 | repeatIndex, 1329 | bindObject, 1330 | element, 1331 | bindName, 1332 | bindId 1333 | ) { 1334 | const getDelayedSetter = function(bindArg, setter) { 1335 | if (bindArg.elementBind.throttle) { 1336 | return function(...args) { 1337 | ViewModel.delay( 1338 | bindArg.getVmValue(bindArg.elementBind.throttle), 1339 | bindId, 1340 | function() { 1341 | setter(...args); 1342 | } 1343 | ); 1344 | }; 1345 | } else { 1346 | return setter; 1347 | } 1348 | }; 1349 | const bindArg = { 1350 | autorun: function(f) { 1351 | let fun = function(c) { 1352 | f(bindArg, c); 1353 | }; 1354 | component.vmComputations.push(ViewModel.Tracker.autorun(fun)); 1355 | }, 1356 | component: component, 1357 | element: element, 1358 | elementBind: bindObject, 1359 | bindName: bindName, 1360 | bindValue: bindObject[bindName], 1361 | getVmValue: ViewModel.getVmValueGetter( 1362 | component, 1363 | repeatObject, 1364 | repeatIndex, 1365 | bindObject[bindName] 1366 | ) 1367 | }; 1368 | bindArg.setVmValue = getDelayedSetter( 1369 | bindArg, 1370 | ViewModel.getVmValueSetter( 1371 | component, 1372 | repeatObject, 1373 | repeatIndex, 1374 | bindObject[bindName] 1375 | ) 1376 | ); 1377 | return bindArg; 1378 | } 1379 | 1380 | static throttle(func, wait, options) { 1381 | var context, args, result; 1382 | var timeout = null; 1383 | var previous = 0; 1384 | if (!options) options = {}; 1385 | var later = function() { 1386 | previous = options.leading === false ? 0 : Date.now(); 1387 | timeout = null; 1388 | result = func.apply(context, args); 1389 | if (!timeout) context = args = null; 1390 | }; 1391 | return function() { 1392 | var now = Date.now(); 1393 | if (!previous && options.leading === false) previous = now; 1394 | var remaining = wait - (now - previous); 1395 | context = this; 1396 | args = arguments; 1397 | if (remaining <= 0 || remaining > wait) { 1398 | if (timeout) { 1399 | clearTimeout(timeout); 1400 | timeout = null; 1401 | } 1402 | previous = now; 1403 | result = func.apply(context, args); 1404 | if (!timeout) context = args = null; 1405 | } else if (!timeout && options.trailing !== false) { 1406 | timeout = setTimeout(later, remaining); 1407 | } 1408 | return result; 1409 | }; 1410 | } 1411 | static signalContainer(containerName, container) { 1412 | const all = []; 1413 | if (containerName) { 1414 | const signalObject = ViewModel.signals[containerName]; 1415 | for (let key in signalObject) { 1416 | let value = signalObject[key]; 1417 | (function(key, value) { 1418 | const single = {}; 1419 | single[key] = {}; 1420 | const transform = 1421 | value.transform || 1422 | function(e) { 1423 | return e; 1424 | }; 1425 | const boundProp = `_${key}_Bound`; 1426 | single.onCreated = function() { 1427 | const vmProp = container[key]; 1428 | const func = function(e) { 1429 | vmProp(transform(e)); 1430 | }; 1431 | const funcToUse = value.throttle 1432 | ? ViewModel.throttle(func, value.throttle) 1433 | : func; 1434 | container[boundProp] = funcToUse; 1435 | value.target.addEventListener(value.event, this[boundProp]); 1436 | var event = document.createEvent("HTMLEvents"); 1437 | event.initEvent(value.event, true, false); 1438 | value.target.dispatchEvent(event); 1439 | }; 1440 | single.onDestroyed = function() { 1441 | value.target.removeEventListener(value.event, this[boundProp]); 1442 | }; 1443 | all.push(single); 1444 | })(key, value); 1445 | } 1446 | } 1447 | return all; 1448 | } 1449 | 1450 | static signalsToLoad(containerName, container) { 1451 | if (containerName instanceof Array) { 1452 | const signals = []; 1453 | for (let name of containerName) { 1454 | for (let signal of ViewModel.signalContainer(name, container)) { 1455 | signals.push(signal); 1456 | } 1457 | } 1458 | return signals; 1459 | } else { 1460 | return ViewModel.signalContainer(containerName, container); 1461 | } 1462 | } 1463 | 1464 | static loadComponent(initial) { 1465 | const vm = {}; 1466 | ViewModel.prepareComponent("TestComponent", vm, initial); 1467 | return vm; 1468 | } 1469 | 1470 | static data() { 1471 | if (!ViewModel.rootComponents) { 1472 | ViewModel.prepareRoot(); 1473 | } 1474 | const allComponents = {}; 1475 | for (let component of ViewModel.rootComponents) { 1476 | ViewModel.fillTree(allComponents, component); 1477 | } 1478 | return allComponents; 1479 | } 1480 | 1481 | static fillTree(allComponents, component) { 1482 | if (component.vmComponentName === "ViewModelExplorer") return; 1483 | const data = component.data(); 1484 | if (Object.keys(data).length > 0) { 1485 | allComponents[ViewModel.getPathToRoot(component)] = data; 1486 | } 1487 | for (let child of component.children()) { 1488 | ViewModel.fillTree(allComponents, child); 1489 | } 1490 | } 1491 | 1492 | static load(allData) { 1493 | if (!ViewModel.rootComponents) { 1494 | ViewModel.prepareRoot(); 1495 | } 1496 | ViewModel.Tracker.nonreactive(function() { 1497 | for (let component of ViewModel.rootComponents) { 1498 | ViewModel.loadComponentState(allData, component); 1499 | } 1500 | }); 1501 | } 1502 | 1503 | static loadComponentState(allData, component) { 1504 | ViewModel.Tracker.afterFlush(() => { 1505 | const data = allData[ViewModel.getPathToRoot(component)]; 1506 | if (data) { 1507 | component.load(data); 1508 | } 1509 | 1510 | for (let child of component.children()) { 1511 | this.loadComponentState(allData, child); 1512 | } 1513 | }); 1514 | } 1515 | } 1516 | 1517 | ViewModel.Tracker = Tracker; 1518 | 1519 | // These are view model properties the user can use 1520 | // but they have special meaning to ViewModel 1521 | ViewModel.properties = { 1522 | autorun: 1, 1523 | events: 1, 1524 | share: 1, 1525 | mixin: 1, 1526 | signal: 1, 1527 | load: 1, 1528 | rendered: 1, 1529 | created: 1, 1530 | destroyed: 1, 1531 | ref: 1 1532 | }; 1533 | 1534 | // The user can't use these properties 1535 | // when defining a view model 1536 | ViewModel.reserved = { 1537 | vmId: 1, 1538 | vmPathToParent: 1, 1539 | vmOnCreated: 1, 1540 | vmOnRendered: 1, 1541 | vmOnDestroyed: 1, 1542 | vmAutorun: 1, 1543 | vmEvents: 1, 1544 | vmInitial: 1, 1545 | vmPropId: 1, 1546 | vmMounted: 1, 1547 | vmElementBind: 1, 1548 | vmChange: 1, 1549 | templateInstance: 1, 1550 | parent: 1, 1551 | children: 1, 1552 | child: 1, 1553 | reset: 1, 1554 | data: 1, 1555 | "data-vm-parent": 1, 1556 | "data-bind": 1 1557 | }; 1558 | 1559 | ViewModel.reactKeyword = { 1560 | render: 1, 1561 | state: 1, 1562 | constructor: 1, 1563 | forceUpdate: 1, 1564 | setState: 1, 1565 | componentWillReceiveProps: 1, 1566 | shouldComponentUpdate: 1, 1567 | componentWillUpdate: 1, 1568 | componentDidUpdate: 1, 1569 | componentWillMount: 1, 1570 | componentDidMount: 1, 1571 | componentWillUnmount: 1 1572 | }; 1573 | 1574 | ViewModel.funPropReserved = { 1575 | valid: 1, 1576 | validMessage: 1, 1577 | invalid: 1, 1578 | invalidMessage: 1, 1579 | validatingMessage: 1, 1580 | validating: 1, 1581 | validator: 1, 1582 | message: 1, 1583 | reset: 1 1584 | }; 1585 | 1586 | ViewModel.compiledBindings = { 1587 | text: 1, 1588 | html: 1, 1589 | class: 1, 1590 | if: 1, 1591 | style: 1, 1592 | repeat: 1, 1593 | key: 1 1594 | }; 1595 | 1596 | ViewModel.globals = []; 1597 | ViewModel.components = {}; 1598 | ViewModel.mixins = {}; 1599 | ViewModel.shared = {}; 1600 | ViewModel.signals = {}; 1601 | ViewModel.bindings = {}; 1602 | 1603 | Object.defineProperties(ViewModel, { 1604 | property: { 1605 | get: function() { 1606 | return new Property(); 1607 | } 1608 | } 1609 | }); 1610 | 1611 | ViewModel.Property = Property; 1612 | ViewModel.saveUrl = getSaveUrl(ViewModel); 1613 | ViewModel.loadUrl = getLoadUrl(ViewModel); 1614 | 1615 | for (let binding of presetBindings) { 1616 | ViewModel.addBinding(binding); 1617 | } 1618 | 1619 | const delayed = {}; 1620 | 1621 | ViewModel.delay = function(time, nameOrFunc, fn) { 1622 | var d, func, id, name; 1623 | func = fn || nameOrFunc; 1624 | if (fn) { 1625 | name = nameOrFunc; 1626 | } 1627 | if (name) { 1628 | d = delayed[name]; 1629 | } 1630 | if (d != null) { 1631 | clearTimeout(d); 1632 | } 1633 | id = setTimeout(func, time); 1634 | if (name) { 1635 | return (delayed[name] = id); 1636 | } 1637 | }; 1638 | -------------------------------------------------------------------------------- /test/viewmodel.test.js: -------------------------------------------------------------------------------- 1 | var ViewModel = require("../dist/viewmodel"); 2 | 3 | describe("share", () => { 4 | beforeEach(() => { 5 | ViewModel.share({ house: { address: "123" } }); 6 | }); 7 | it("adds property to component", () => { 8 | var vm = ViewModel.loadComponent({ share: "house" }); 9 | var result = vm.address(); 10 | expect(result).toBe("123"); 11 | }); 12 | it("modifying property in one component modifies the same prop in another comp", () => { 13 | var vm1 = ViewModel.loadComponent({ share: "house" }); 14 | var vm2 = ViewModel.loadComponent({ share: "house" }); 15 | vm1.address("ABC"); 16 | expect(vm1.address()).toBe(vm2.address()); 17 | }); 18 | it("doesn't change shared property default value", () => { 19 | var vm = ViewModel.loadComponent({ share: "house", address: "XYZ" }); 20 | var result = vm.address(); 21 | expect(result).toBe("123"); 22 | }); 23 | }); 24 | 25 | describe("mixin", () => { 26 | beforeEach(() => { 27 | ViewModel.mixin({ house: { address: "ABC" } }); 28 | }); 29 | it("adds property to component", () => { 30 | var vm = ViewModel.loadComponent({ mixin: "house" }); 31 | var result = vm.address(); 32 | expect(result).toBe("ABC"); 33 | }); 34 | it("modifying property in one component doesn't modify the same prop in another comp", () => { 35 | var vm1 = ViewModel.loadComponent({ mixin: "house" }); 36 | var vm2 = ViewModel.loadComponent({ mixin: "house" }); 37 | vm1.address("123"); 38 | expect(vm1.address()).toBe("123"); 39 | expect(vm2.address()).toBe("ABC"); 40 | }); 41 | it("changes mixin property default value", () => { 42 | var vm = ViewModel.loadComponent({ mixin: "house", address: "XYZ" }); 43 | var result = vm.address(); 44 | expect(result).toBe("XYZ"); 45 | }); 46 | }); 47 | 48 | describe("data/load", () => { 49 | beforeEach(function() { 50 | jest.useFakeTimers(); 51 | }); 52 | it("retrieves and loads state", () => { 53 | var vm1 = ViewModel.loadComponent({ name: "Alan" }); 54 | ViewModel.rootComponents = [vm1]; 55 | var data = ViewModel.data(); 56 | vm1.name("Brito"); 57 | ViewModel.load(data); 58 | jest.runAllTimers(); 59 | expect(vm1.name()).toBe("Alan"); 60 | }); 61 | }); 62 | 63 | describe("getPathToRoot", () => { 64 | it("returns root if it doesn't have a parent", () => { 65 | var vm1 = ViewModel.loadComponent({ name: "Alan" }); 66 | var path = ViewModel.getPathToRoot(vm1); 67 | expect(path).toBe("TestComponent/"); 68 | }); 69 | }); 70 | 71 | describe("Properties", () => { 72 | describe("beforeUpdate", () => { 73 | it("sets component as this/context", () => { 74 | var context = null; 75 | var newValue = ""; 76 | var oldValue = ""; 77 | var vm = ViewModel.loadComponent({ 78 | name: ViewModel.property.string 79 | .default("B") 80 | .beforeUpdate(function(nextValue) { 81 | context = this; 82 | newValue = nextValue; 83 | oldValue = this.name(); 84 | }) 85 | }); 86 | expect(vm.vmChanged).toBeFalsy(); 87 | vm.name("A"); 88 | expect(vm.vmChanged).toBe(true); 89 | expect(context).toBe(vm); 90 | expect(newValue).toBe("A"); 91 | expect(oldValue).toBe("B"); 92 | }); 93 | 94 | it("sets context from direct share", () => { 95 | var context = null; 96 | var newValue = ""; 97 | var oldValue = ""; 98 | 99 | ViewModel.share({ 100 | bfu: { 101 | name: ViewModel.property.string 102 | .default("B") 103 | .beforeUpdate(function(nextValue) { 104 | context = this; 105 | newValue = nextValue; 106 | oldValue = this.name(); 107 | }) 108 | } 109 | }); 110 | 111 | var vm = ViewModel.loadComponent({ 112 | share: "bfu" 113 | }); 114 | expect(vm.vmChanged).toBeFalsy(); 115 | vm.name("A"); 116 | expect(vm.vmChanged).toBe(true); 117 | expect(context).toBe(ViewModel.shared["bfu"]); 118 | expect(newValue).toBe("A"); 119 | expect(oldValue).toBe("B"); 120 | }); 121 | 122 | it("sets context from scoped share", () => { 123 | var context = null; 124 | var newValue = ""; 125 | var oldValue = ""; 126 | 127 | ViewModel.share({ 128 | bfu1: { 129 | name: ViewModel.property.string 130 | .default("B") 131 | .beforeUpdate(function(nextValue) { 132 | context = this; 133 | newValue = nextValue; 134 | oldValue = this.name(); 135 | }) 136 | } 137 | }); 138 | 139 | var vm = ViewModel.loadComponent({ share: { foo: "bfu1" } }); 140 | expect(vm.vmChanged).toBeFalsy(); 141 | vm.foo.name("A"); 142 | expect(vm.vmChanged).toBe(true); 143 | expect(context).toBe(ViewModel.shared["bfu1"]); 144 | expect(newValue).toBe("A"); 145 | expect(oldValue).toBe("B"); 146 | }); 147 | 148 | it("sets context from direct mixin", () => { 149 | var context = null; 150 | var newValue = ""; 151 | var oldValue = ""; 152 | 153 | ViewModel.mixin({ 154 | bfu: { 155 | name: ViewModel.property.string 156 | .default("B") 157 | .beforeUpdate(function(nextValue) { 158 | context = this; 159 | newValue = nextValue; 160 | oldValue = this.name(); 161 | }) 162 | } 163 | }); 164 | 165 | var vm = ViewModel.loadComponent({ mixin: "bfu" }); 166 | expect(vm.vmChanged).toBeFalsy(); 167 | vm.name("A"); 168 | expect(vm.vmChanged).toBe(true); 169 | expect(context).toBe(vm); 170 | expect(newValue).toBe("A"); 171 | expect(oldValue).toBe("B"); 172 | }); 173 | 174 | it("sets context from scoped mixin", () => { 175 | var newValue = ""; 176 | var oldValue = ""; 177 | 178 | ViewModel.mixin({ 179 | bfu1: { 180 | name: ViewModel.property.string 181 | .default("B") 182 | .beforeUpdate(function(nextValue) { 183 | newValue = nextValue; 184 | oldValue = this.name(); 185 | }) 186 | } 187 | }); 188 | 189 | var vm = ViewModel.loadComponent({ 190 | mixin: { foo: "bfu1" } 191 | }); 192 | expect(vm.vmChanged).toBeFalsy(); 193 | vm.foo.name("A"); 194 | expect(vm.vmChanged).toBe(true); 195 | expect(newValue).toBe("A"); 196 | expect(oldValue).toBe("B"); 197 | }); 198 | }); 199 | 200 | describe("afterUpdate", () => { 201 | it("sets component as this/context", () => { 202 | var context = null; 203 | var newValue = ""; 204 | var oldValue = ""; 205 | var vm = ViewModel.loadComponent({ 206 | name: ViewModel.property.string 207 | .default("B") 208 | .afterUpdate(function(value) { 209 | context = this; 210 | oldValue = value; 211 | newValue = this.name(); 212 | }) 213 | }); 214 | expect(vm.vmChanged).toBeFalsy(); 215 | vm.name("A"); 216 | expect(vm.vmChanged).toBe(true); 217 | expect(context).toBe(vm); 218 | expect(newValue).toBe("A"); 219 | expect(oldValue).toBe("B"); 220 | }); 221 | 222 | it("sets context from direct share", () => { 223 | var context = null; 224 | var newValue = ""; 225 | var oldValue = ""; 226 | 227 | ViewModel.share({ 228 | bfu: { 229 | name: ViewModel.property.string 230 | .default("B") 231 | .afterUpdate(function(value) { 232 | context = this; 233 | oldValue = value; 234 | newValue = this.name(); 235 | }) 236 | } 237 | }); 238 | 239 | var vm = ViewModel.loadComponent({ share: "bfu" }); 240 | expect(vm.vmChanged).toBeFalsy(); 241 | vm.name("A"); 242 | expect(vm.vmChanged).toBe(true); 243 | expect(context).toBe(ViewModel.shared["bfu"]); 244 | expect(newValue).toBe("A"); 245 | expect(oldValue).toBe("B"); 246 | }); 247 | 248 | it("sets context from scoped share", () => { 249 | var context = null; 250 | var newValue = ""; 251 | var oldValue = ""; 252 | 253 | ViewModel.share({ 254 | bfu1: { 255 | name: ViewModel.property.string 256 | .default("B") 257 | .afterUpdate(function(value) { 258 | context = this; 259 | oldValue = value; 260 | newValue = this.name(); 261 | }) 262 | } 263 | }); 264 | 265 | var vm = ViewModel.loadComponent({ share: { foo: "bfu1" } }); 266 | expect(vm.vmChanged).toBeFalsy(); 267 | vm.foo.name("A"); 268 | expect(vm.vmChanged).toBe(true); 269 | expect(context).toBe(ViewModel.shared["bfu1"]); 270 | expect(newValue).toBe("A"); 271 | expect(oldValue).toBe("B"); 272 | }); 273 | 274 | it("sets context from direct mixin", () => { 275 | var context = null; 276 | var newValue = ""; 277 | var oldValue = ""; 278 | 279 | ViewModel.mixin({ 280 | bfu: { 281 | name: ViewModel.property.string 282 | .default("B") 283 | .afterUpdate(function(value) { 284 | context = this; 285 | oldValue = value; 286 | newValue = this.name(); 287 | }) 288 | } 289 | }); 290 | 291 | var vm = ViewModel.loadComponent({ mixin: "bfu" }); 292 | expect(vm.vmChanged).toBeFalsy(); 293 | vm.name("A"); 294 | expect(vm.vmChanged).toBe(true); 295 | expect(context).toBe(vm); 296 | expect(newValue).toBe("A"); 297 | expect(oldValue).toBe("B"); 298 | }); 299 | 300 | it("sets context from scoped mixin", () => { 301 | var newValue = ""; 302 | var oldValue = ""; 303 | 304 | ViewModel.mixin({ 305 | bfu1: { 306 | name: ViewModel.property.string 307 | .default("B") 308 | .afterUpdate(function(value) { 309 | oldValue = value; 310 | newValue = this.name(); 311 | }) 312 | } 313 | }); 314 | 315 | var vm = ViewModel.loadComponent({ mixin: { foo: "bfu1" } }); 316 | expect(vm.vmChanged).toBeFalsy(); 317 | vm.foo.name("A"); 318 | expect(vm.vmChanged).toBe(true); 319 | expect(newValue).toBe("A"); 320 | expect(oldValue).toBe("B"); 321 | }); 322 | }); 323 | }); 324 | --------------------------------------------------------------------------------