├── .gitignore ├── .jshintrc ├── .travis.yml ├── Copy.js ├── Element.js ├── LICENSE ├── README.md ├── Renderer.js ├── Variable.js ├── assets ├── alkali-logo-old.png ├── alkali-logo.svg └── powers-dre.png ├── bin └── makeProperties.js ├── bower.json ├── dist ├── index.js └── index.js.map ├── extensions ├── async-hooks.js ├── dstore.js └── typescript.js ├── index.d.ts ├── index.js ├── operators.js ├── package.js ├── package.json ├── reactive.js ├── tests ├── ContextualPromise.js ├── Copy.js ├── Element.js ├── Renderer.js ├── Variable.js ├── all.js ├── dgrid.js ├── dstore.js ├── es6.js ├── form.html ├── has.js ├── intern.js ├── operators.js ├── reactive.js ├── test.html ├── tsconfig.json ├── types.ts └── unit.js ├── tsconfig.json ├── util ├── ContextualPromise.js └── lang.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "bitwise": false, 4 | "boss": false, 5 | "browser": true, 6 | "camelcase": true, 7 | "couch": false, 8 | "curly": true, 9 | "debug": false, 10 | "devel": true, 11 | "dojo": false, 12 | "eqeqeq": false, 13 | "eqnull": true, 14 | "es3": true, 15 | "esnext": false, 16 | "evil": false, 17 | "expr": true, 18 | "forin": false, 19 | "funcscope": true, 20 | "gcl": false, 21 | "globalstrict": false, 22 | "immed": true, 23 | "iterator": false, 24 | "jquery": false, 25 | "lastsemic": false, 26 | "latedef": false, 27 | "laxbreak": true, 28 | "laxcomma": false, 29 | "loopfunc": true, 30 | "mootools": false, 31 | "moz": false, 32 | "multistr": false, 33 | "newcap": true, 34 | "noarg": true, 35 | "node": false, 36 | "noempty": false, 37 | "nonew": true, 38 | "nonstandard": false, 39 | "nomen": false, 40 | "onecase": false, 41 | "onevar": false, 42 | "passfail": false, 43 | "phantom": false, 44 | "plusplus": false, 45 | "proto": true, 46 | "prototypejs": false, 47 | "regexdash": true, 48 | "regexp": false, 49 | "rhino": false, 50 | "scripturl": true, 51 | "shadow": true, 52 | "shelljs": false, 53 | "smarttabs": true, 54 | "strict": false, 55 | "sub": false, 56 | "supernew": false, 57 | "trailing": true, 58 | "undef": true, 59 | "unused": true, 60 | "validthis": true, 61 | "withstmt": false, 62 | "worker": false, 63 | "wsh": false, 64 | "yui": false, 65 | "white": false, 66 | 67 | "maxlen": 140, 68 | "indent": 4, 69 | "maxerr": 250, 70 | "predef": [ "require", "define" ], 71 | "quotmark": "single", 72 | "maxcomplexity": 20 73 | } 74 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8.9" 5 | cache: 6 | directories: 7 | - node_modules 8 | env: 9 | global: 10 | - SAUCE_USERNAME: kriszyp 11 | - SAUCE_ACCESS_KEY: 468f7823-0321-4b0f-a3e3-3b25899c7c80 12 | install: 13 | - travis_retry npm install 14 | script: npm test 15 | -------------------------------------------------------------------------------- /Copy.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 2 | define(['./util/lang', './Variable'], factory) } else if (typeof module === 'object' && module.exports) { 3 | module.exports = factory(require('./util/lang'), require('./Variable')) // Node 4 | }}(this, function (lang, VariableExports) { 5 | var Variable = VariableExports.Variable 6 | 7 | function deepCopy(source, target, derivativeMap) { 8 | if (source && typeof source == 'object') { 9 | if (source instanceof Array) { 10 | target = [] // always create a new array for array targets 11 | for(var i = 0, l = source.length; i < l; i++) { 12 | target[i] = deepCopy(source[i], null, derivativeMap) 13 | } 14 | } else { 15 | if (!target || typeof target !== 'object') { 16 | target = derivativeMap && derivativeMap.get(source) 17 | if (!target) { 18 | target = {} 19 | derivativeMap && derivativeMap.set(source, target) 20 | } 21 | } 22 | for (var i in source) { 23 | target[i] = deepCopy(source[i], target[i], derivativeMap) 24 | } 25 | } 26 | return target 27 | } 28 | return source 29 | } 30 | 31 | var Copy = lang.compose(Variable, function(copiedFrom) { 32 | // this is the variable that we derive from 33 | this.copiedFrom = copiedFrom 34 | this.derivativeMap = new lang.WeakMap(null, 'derivative') 35 | this.isDirty = new Variable(false) 36 | }, { 37 | getValue: function(sync, forModification, forChild) { 38 | if(this.state) { 39 | this.state = null 40 | } 41 | var value = this.copiedFrom.valueOf() 42 | if(value && typeof value == 'object') { 43 | var derivative = this.derivativeMap.get(value) 44 | if (derivative == null) { 45 | this.derivativeMap.set(value, derivative = deepCopy(value, undefined, this.derivativeMap)) 46 | this.value = derivative 47 | } 48 | } 49 | if(this.value === undefined) { 50 | return value 51 | } 52 | return Variable.prototype.getValue.call(this, sync, forModification, forChild) 53 | }, 54 | getCopyOf: function(value) { 55 | var derivative = this.derivativeMap.get(value) 56 | if (derivative == null) { 57 | this.derivativeMap.set(value, derivative = deepCopy(value, undefined, this.derivativeMap)) 58 | } 59 | return derivative 60 | }, 61 | save: function() { 62 | // copy back to the original object 63 | if (this.copiedFrom.put) { // copiedFrom doesn't have to be a variable, it can be a plain object 64 | // just assign it now 65 | this.copiedFrom.put(this.valueOf()) 66 | } else { 67 | // copy back into the original object 68 | var original = this.copiedFrom.valueOf() 69 | var newCopiedFrom = deepCopy(this.valueOf(), original) 70 | } 71 | this.isDirty.put(false) 72 | this.onSave && this.onSave() 73 | }, 74 | revert: function() { 75 | var original = this.copiedFrom.valueOf() 76 | this.derivativeMap = new lang.WeakMap(null, 'derivative') // clear out the mapping, so we can start fresh 77 | this.put(deepCopy(original, undefined, this.derivativeMap)) 78 | this.isDirty.put(false) 79 | }, 80 | updated: function() { 81 | this.isDirty.put(true) 82 | return Variable.prototype.updated.apply(this, arguments) 83 | } 84 | }) 85 | return Copy 86 | })) 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kris Zyp 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 | -------------------------------------------------------------------------------- /Renderer.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 2 | define(['./util/lang', './Variable'], factory) } else if (typeof module === 'object' && module.exports) { 3 | module.exports = factory(require('./util/lang'), require('./Variable')) // Node 4 | }}(this, function (lang, VariableExports) { 5 | var doc = typeof document !== 'undefined' && document 6 | var invalidatedElements 7 | var queued 8 | var toRender = [] 9 | var nextId = 1 10 | var rAFOrdered = lang.rAFOrdered 11 | var Context = VariableExports.Context 12 | 13 | function Renderer(options) { 14 | var variable = options.variable 15 | 16 | this.variable = variable 17 | if (options.selector) { 18 | this.selector = options.selector 19 | } 20 | if (options.getElements) { 21 | this.getElements = options.getElements 22 | } 23 | else if (options.element) { 24 | var element = this.element = options.element; 25 | (element.alkaliRenderers || (element.alkaliRenderers = [])).push(this) 26 | } else { 27 | throw new Error('No element provided to Renderer') 28 | } 29 | if (options.update) { 30 | this.updateRendering = options.update 31 | } 32 | if (options.shouldRender) { 33 | this.shouldRender = options.shouldRender 34 | } 35 | if (options.renderUpdate) { 36 | this.renderUpdate = options.renderUpdate 37 | } 38 | if (options.alwaysUpdate) { 39 | this.alwaysUpdate = options.alwaysUpdate 40 | } 41 | if (options.updateOnStart === false){ 42 | var contextualized = this.contextualized || this.variable 43 | this.variable.valueOf(this) 44 | // even if we don't render on start, we still need to compute the value so we can depend on the computed 45 | // TODO: we may need to handle recontextualization if it returns a promise 46 | contextualized.notifies(this) 47 | } else if (element) { 48 | this.updateRendering(true) 49 | } else { 50 | // bound to a class, just do notification 51 | this.variable.notifies(this) 52 | } 53 | } 54 | Renderer.prototype = { 55 | constructor: Renderer, 56 | version: 0, 57 | notifies: true, 58 | updateRendering: function () { 59 | throw new Error ('updateRendering must be implemented by sub class of Renderer') 60 | }, 61 | updated: function (updateEvent, by, context) { 62 | if (!this.invalidated) { 63 | if (this.getElements) { 64 | var variable = this.variable 65 | var invalidated = this.invalidated || (this.invalidated = new lang.Set()) 66 | this.getElements().forEach(function(element) { 67 | if (!updateEvent.doesAffect || updateEvent.doesAffect(element)){ 68 | invalidated.add(element) 69 | } 70 | /*if (element.constructor.getForClass(element, variable) == by) { 71 | invalidated.add(element) 72 | }*/ 73 | }) 74 | } else { 75 | // do this only once, until we render again 76 | this.invalidated = true 77 | } 78 | if (this.deferredRender) { 79 | this.deferredRender.isCanceled = true 80 | this.deferredRender = null 81 | } 82 | var renderer = this; 83 | (updateEvent.visited.enqueueUpdate || rAFOrdered)(function() { 84 | if (renderer.invalidated === true) { 85 | renderer.updateRendering(renderer.alwaysUpdate, renderer.element) 86 | } else if (renderer.invalidated) { 87 | renderer.invalidated.forEach(function(element) { 88 | renderer.updateRendering(renderer.alwaysUpdate, element) 89 | }) 90 | } 91 | }, this.element) 92 | } 93 | }, 94 | executeWithin: Context.prototype.executeWithin, 95 | setVersion: function(){ 96 | // this doesn't need its own version/hash 97 | }, 98 | newContext: function() { 99 | return new Context(this.element, true) 100 | }, 101 | getContextualized: function(Variable) { 102 | return Context.prototype.getContextualized.call(this, Variable) 103 | //return this.contextualized || this.variable 104 | }, 105 | specify: function(Variable) { 106 | return this.contextualized = Context.prototype.specify.call(this, Variable) 107 | // a new context to get this 108 | //return this.contextualized = this.newContext(null, true).specify(Variable) 109 | }, 110 | merge: function(){ 111 | // noop 112 | }, 113 | contextMatches: function(context) { 114 | return true 115 | }, 116 | invalidateElement: function(element) { 117 | if(!invalidatedElements){ 118 | invalidatedElements = new WeakMap(null, 'invalidated') 119 | } 120 | var invalidatedParts = invalidatedElements.get(element) 121 | invalidatedElements.set(element, invalidatedParts = {}) 122 | if (!invalidatedParts[id]) { 123 | invalidatedParts[id] = true 124 | } 125 | if (!queued) { 126 | lang.queueTask(processQueue) 127 | queued = true 128 | } 129 | var renderer = this 130 | toRender.push(function(){ 131 | renderer.invalidated = false 132 | renderer.updateElement(element) 133 | }) 134 | }, 135 | getId: function(){ 136 | return this.id || (this.id = nextId++) 137 | }, 138 | stop: function() { 139 | var contextualized = this.contextualized || this.variable 140 | contextualized.stopNotifies(this) 141 | if (this.builtList) { 142 | this.builtList = false 143 | this.omitValueOf = false 144 | } 145 | }, 146 | restart: function() { 147 | this.updateRendering(true) 148 | }, 149 | isSameProperty: function(renderer) { 150 | return renderer.constructor === this.constructor && renderer.name === this.name 151 | } 152 | } 153 | Object.defineProperty(Renderer.prototype, 'subject', { 154 | get: function() { 155 | return this.element 156 | } 157 | }) 158 | 159 | function ElementRenderer(options) { 160 | Renderer.call(this, options) 161 | } 162 | ElementRenderer.prototype = Object.create(Renderer.prototype) 163 | ElementRenderer.prototype.shouldRender = function (element) { 164 | return document.body.contains(element) || typeof element.__alkaliAttached__ != 'boolean' 165 | } 166 | ElementRenderer.prototype.getSubject = function () { 167 | return this.element 168 | } 169 | ElementRenderer.prototype.updateRendering = function (always, element) { 170 | if (!element && this.elements) { 171 | var elements = this.elements 172 | if(!elements.length){ 173 | if(this.selector){ 174 | elements = document.querySelectorAll(this.selector) 175 | }else{ 176 | throw new Error('No element or selector was provided to the Renderer') 177 | } 178 | return 179 | } 180 | for(var i = 0, l = elements.length; i < l; i++){ 181 | this.updateRendering(always, elements[i]) 182 | } 183 | } else { 184 | var thisElement = element || this.element 185 | 186 | if(always || this.shouldRender(thisElement)){ 187 | // it is connected 188 | this.updateElement(thisElement) 189 | } else { 190 | var id = this.getId() 191 | var renderers = thisElement.renderersOnShow 192 | if(!renderers){ 193 | renderers = thisElement.renderersOnShow = [] 194 | thisElement.className += ' needs-rerendering' 195 | } 196 | if (!renderers[id]) { 197 | renderers[id] = this 198 | } 199 | } 200 | } 201 | } 202 | ElementRenderer.prototype.addElement = function (element) { 203 | if (this.selector) { 204 | element.renderersOnShow = [this] 205 | } else { 206 | this.elements.push(element) 207 | } 208 | // and immediately do an update 209 | this.updateElement(element) 210 | } 211 | ElementRenderer.prototype.updateElement = function(element) { 212 | this.invalidated = false 213 | if (this.omitValueOf) { 214 | this.started = true 215 | this.renderUpdate(undefined, element) 216 | return 217 | } 218 | var Element = element.constructor 219 | var generalized = Element._generalized 220 | var resolved 221 | var renderer = element === this.element ? this : Object.create(this, { element: { value: element} }) 222 | var deferredRender 223 | renderer.executeWithin(function() { 224 | deferredRender = renderer.variable.then(function(value) { 225 | resolved = true 226 | if (deferredRender) { 227 | if (deferredRender === renderer.deferredRender) { 228 | renderer.deferredRender = null 229 | } 230 | if (deferredRender.isCanceled) { 231 | return 232 | } 233 | } 234 | if (!renderer.invalidated) { 235 | if (renderer.contextualized && renderer.contextualized !== renderer.variable) { 236 | renderer.contextualized.stopNotifies(renderer) 237 | } 238 | if (generalized) { 239 | var rendererIdentifier = renderer.toString() 240 | if (!generalized[rendererIdentifier]) { 241 | generalized[rendererIdentifier] = true; 242 | (renderer.contextualized = renderer.variable).notifies(renderer) 243 | } 244 | } else { 245 | renderer.executeWithin(function() { 246 | renderer.contextualized = renderer.variable.notifies(renderer) 247 | }) 248 | } 249 | if(value !== undefined || renderer.started) { 250 | renderer.started = true 251 | renderer.renderUpdate(value, element) 252 | } 253 | } 254 | }, function(error) { 255 | console.error('Error rendering', renderer, error) 256 | }) 257 | }) 258 | if(!resolved){ 259 | // start listening for changes immediately 260 | if (generalized) { 261 | var rendererIdentifier = renderer.toString() 262 | if (!generalized[rendererIdentifier]) { 263 | generalized[rendererIdentifier] = true; 264 | (renderer.contextualized = renderer.variable).notifies(renderer) 265 | } 266 | } else { 267 | renderer.executeWithin(function() { 268 | renderer.contextualized = renderer.variable.notifies(renderer) 269 | }) 270 | } 271 | this.deferredRender = deferredRender 272 | if (this.renderLoading) { 273 | // if we have loading renderer call it 274 | this.renderLoading(deferredRender, element) 275 | } 276 | } 277 | } 278 | ElementRenderer.prototype.renderUpdate = function (newValue, element) { 279 | throw new Error('renderUpdate(newValue) must be implemented') 280 | } 281 | Renderer.Renderer = Renderer 282 | Renderer.ElementRenderer = ElementRenderer 283 | 284 | function AttributeRenderer(options) { 285 | if(options.name){ 286 | this.name = options.name 287 | } 288 | ElementRenderer.apply(this, arguments) 289 | } 290 | AttributeRenderer.prototype = Object.create(ElementRenderer.prototype) 291 | AttributeRenderer.prototype.type = 'AttributeRenderer' 292 | AttributeRenderer.prototype.renderUpdate = function (newValue, element) { 293 | if (typeof newValue == 'boolean' || newValue == null) { 294 | // for booleans or null/undefined, treat the attribute boolean-like, setting and removing 295 | if (newValue) { 296 | element.setAttribute(this.name, '') // "set" the attribute to enabled 297 | } else { 298 | element.removeAttribute(this.name) // disable the attribute, removing it 299 | } 300 | } else { 301 | // otherwise, assign value as string 302 | element.setAttribute(this.name, newValue) 303 | } 304 | } 305 | Renderer.AttributeRenderer = AttributeRenderer 306 | 307 | function PropertyRenderer(options) { 308 | if (options.name) { 309 | this.name = options.name 310 | } 311 | ElementRenderer.apply(this, arguments) 312 | } 313 | PropertyRenderer.prototype = Object.create(ElementRenderer.prototype) 314 | PropertyRenderer.prototype.type = 'PropertyRenderer' 315 | PropertyRenderer.prototype.renderUpdate = function (newValue, element) { 316 | element[this.name] = newValue 317 | } 318 | Renderer.PropertyRenderer = PropertyRenderer 319 | 320 | function InputPropertyRenderer(options) { 321 | if (options.element && options.element.tagName === 'SELECT' && options.name === 'value') { 322 | // use the deferred value assignment for , we may need to wait until the children are constructed 345 | element.eventualValue = newValue 346 | lang.nextTurn(function() { 347 | if (element.eventualValue) { 348 | element.value = element.eventualValue 349 | element.eventualValue = undefined 350 | } 351 | }) 352 | } else { 353 | element.eventualValue = undefined 354 | } 355 | } 356 | Renderer.InputPropertyRenderer = InputPropertyRenderer 357 | 358 | function StyleRenderer(options) { 359 | if(options.name){ 360 | this.name = options.name 361 | } 362 | ElementRenderer.apply(this, arguments) 363 | } 364 | StyleRenderer.prototype = Object.create(ElementRenderer.prototype) 365 | StyleRenderer.prototype.type = 'StyleRenderer' 366 | StyleRenderer.prototype.renderUpdate = function (newValue, element) { 367 | element.style[this.name] = newValue 368 | } 369 | Renderer.StyleRenderer = StyleRenderer 370 | 371 | function ContentRenderer(options) { 372 | ElementRenderer.apply(this, arguments) 373 | } 374 | ContentRenderer.prototype = Object.create(ElementRenderer.prototype) 375 | ContentRenderer.prototype.type = 'ContentRenderer' 376 | ContentRenderer.prototype.renderUpdate = function (newValue, element) { 377 | element.innerHTML = '' 378 | if (newValue === undefined){ 379 | newValue = '' 380 | } 381 | element.appendChild(document.createTextNode(newValue)) 382 | } 383 | Renderer.ContentRenderer = ContentRenderer 384 | 385 | function TextRenderer(options) { 386 | this.position = options.position 387 | this.textNode = options.textNode 388 | ElementRenderer.apply(this, arguments) 389 | } 390 | TextRenderer.prototype = Object.create(ElementRenderer.prototype) 391 | TextRenderer.prototype.type = 'TextRenderer' 392 | TextRenderer.prototype.updated = function (updateEvent, context) { 393 | if (this.builtList) { 394 | if (updateEvent.type === 'replaced') { 395 | this.builtList = false 396 | this.omitValueOf = false 397 | } else { 398 | (this.updates || (this.updates = [])).push(updateEvent) 399 | } 400 | } 401 | ElementRenderer.prototype.updated.call(this, updateEvent, context) 402 | } 403 | TextRenderer.prototype.renderUpdate = function (newValue, element) { 404 | if (newValue == null){ 405 | newValue = '' 406 | } 407 | if (newValue.create) { 408 | newValue = newValue.create({parent: element}) 409 | } 410 | if (newValue.nodeType) { 411 | if (this.textNode && this.textNode.parentNode == element) { 412 | // text node is attached, we can replace it with the node 413 | element.replaceChild(newValue, this.textNode) 414 | } else { 415 | element.appendChild(newValue) 416 | } 417 | this.textNode = newValue 418 | } else if (newValue instanceof Array) { 419 | this.renderUpdate = ListRenderer.prototype.renderUpdate 420 | this.omitValueOf = true 421 | this.renderUpdate(newValue, element) 422 | } else { 423 | (this.textNode || element.childNodes[this.position]).nodeValue = newValue 424 | } 425 | } 426 | Renderer.TextRenderer = TextRenderer 427 | 428 | function ListRenderer(options) { 429 | if (options.each) { 430 | this.each = options.each 431 | } 432 | ElementRenderer.apply(this, arguments) 433 | } 434 | ListRenderer.prototype = Object.create(ElementRenderer.prototype) 435 | ListRenderer.prototype.updated = function (updateEvent, context) { 436 | if (this.builtList) { 437 | if (updateEvent.type === 'replaced') { 438 | this.builtList = false 439 | this.omitValueOf = false 440 | } else { 441 | (this.updates || (this.updates = [])).push(updateEvent) 442 | } 443 | } 444 | ElementRenderer.prototype.updated.call(this, updateEvent, context) 445 | } 446 | ListRenderer.prototype.type = 'ListRenderer' 447 | ListRenderer.prototype.renderUpdate = function (newValue, element) { 448 | var container 449 | var each = this.each || function(item) { // TODO: make a single identity function 450 | return item 451 | } 452 | var thisElement = this.element 453 | var renderer = this 454 | if (!this.builtList) { 455 | this.builtList = true 456 | this.omitValueOf = true 457 | element.innerHTML = '' 458 | var childElements = this.childElements = [] 459 | if (each.defineHasOwn) { 460 | each.defineHasOwn() 461 | } 462 | if (newValue) { 463 | var renderEach = function(item) { 464 | childElements.push(Renderer.append(thisElement, eachItem(item))) 465 | } 466 | var variable = this.variable 467 | if (variable.collectionOf) { 468 | this.executeWithin(function() { 469 | variable.forEach(renderEach) 470 | }) 471 | } else if (newValue.forEach) { 472 | newValue.forEach(renderEach) 473 | } else { 474 | this.renderUpdate = TextRenderer.prototype.renderUpdate 475 | this.omitValueOf = false 476 | return this.renderUpdate(newValue, element) 477 | } 478 | } 479 | var contextualized = this.contextualized || this.variable 480 | contextualized.notifies(this) 481 | 482 | // TODO: restore using a doc fragment to add these items: 483 | // thisElement.appendChild(container) 484 | } else { 485 | var childElements = this.childElements 486 | var updates = this.updates || [{ type: 'replaced' }] 487 | container = thisElement 488 | updates.forEach(function(update) { 489 | if (update.type === 'replaced') { 490 | renderer.builtList = false 491 | for (var i = 0, l = childElements.length; i < l; i++) { 492 | thisElement.removeChild(childElements[i]) 493 | } 494 | renderer.updateElement(thisElement) 495 | } else if (update.type === 'spliced') { 496 | var index = update.start 497 | for (var i = 0; i < update.deleteCount; i++) { 498 | thisElement.removeChild(childElements[update.start + i]) 499 | } 500 | childElements.splice(update.start, update.deleteCount) 501 | for (var i = 0; i < update.items.length; i++) { 502 | var value = update.items[i] 503 | var nextChild = childElements[i + update.start] 504 | var newElement = Renderer.append(thisElement, eachItem(value)) 505 | if (nextChild) { 506 | thisElement.insertBefore(newElement, nextChild) 507 | childElements.splice(i + update.start, 0, newElement) 508 | } else { 509 | childElements.push(newElement) 510 | } 511 | } 512 | } else if (update.type === 'property') { 513 | // should be handled by sub-renderers 514 | /*thisElement.removeChild(childElements[update.key]) 515 | var nextChild = childElements[update.key + 1] 516 | var newElement = Renderer.append(thisElement, eachItem(update.target)) 517 | thisElement.insertBefore(newElement, nextChild) 518 | childElements[update.key] = newElement*/ 519 | } 520 | }) 521 | this.updates = [] // clear the updates 522 | } 523 | function eachItem(item) { 524 | var childElement 525 | if (each.create) { 526 | childElement = each.create({parent: thisElement, _item: item}) // TODO: make a faster object here potentially 527 | } else { 528 | childElement = each(item, thisElement) 529 | if (childElement && childElement.create) { 530 | childElement = childElement.create({parent: thisElement, _item: item}) 531 | } 532 | } 533 | return childElement 534 | } 535 | } 536 | Renderer.ListRenderer = ListRenderer 537 | 538 | Renderer.onShowElement = function(shownElement){ 539 | rAFOrdered(function(){ 540 | invalidatedElements = null 541 | var elements = [].slice.call(shownElement.getElementsByClassName('needs-rerendering')) 542 | if (shownElement.className.indexOf('needs-rerendering') > 0){ 543 | var includingTop = [shownElement] 544 | includingTop.push.apply(includingTop, elements) 545 | elements = includingTop 546 | } 547 | for (var i = 0, l = elements.length; i < l; i++){ 548 | var element = elements[i] 549 | var renderers = element.renderersOnShow 550 | if(renderers){ 551 | element.renderersOnShow = null 552 | // remove needs-rerendering class 553 | element.className = element.className.replace(/\s?needs\-rerendering/g, '') 554 | for (var id in renderers) { 555 | var renderer = renderers[id] 556 | if (renderer.omitValueOf) 557 | renderer.omitValueOf = false 558 | renderer.updateElement(element) 559 | } 560 | } 561 | } 562 | }) 563 | } 564 | 565 | function onElementRemoval(element){ 566 | // cleanup element renderers 567 | if(element.alkaliRenderers){ 568 | var renderers = element.alkaliRenderers 569 | for(var i = 0; i < renderers.length; i++){ 570 | var renderer = renderers[i] 571 | renderer.variable.stopNotifies(renderer) 572 | } 573 | } 574 | } 575 | Renderer.onElementRemoval = function(element, onlyChildren){ 576 | if(!onlyChildren){ 577 | onElementRemoval(element) 578 | } 579 | var children = element.getElementsByTagName('*') 580 | for(var i = 0, l = children.length; i < l; i++){ 581 | var child = children[i] 582 | if(child.alkaliRenderers){ 583 | onElementRemoval(child) 584 | } 585 | } 586 | } 587 | return Renderer 588 | })) 589 | -------------------------------------------------------------------------------- /assets/alkali-logo-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/alkali/ff7a9c86e81fe5b4fe5a1eb5a8a4aa5af0029bff/assets/alkali-logo-old.png -------------------------------------------------------------------------------- /assets/alkali-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Alkali A 6 | 7 | 8 | 9 | lkali 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/powers-dre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/alkali/ff7a9c86e81fe5b4fe5a1eb5a8a4aa5af0029bff/assets/powers-dre.png -------------------------------------------------------------------------------- /bin/makeProperties.js: -------------------------------------------------------------------------------- 1 | // A utility script for getting properties from element prototypes 2 | elements = [ 3 | 'Video', 4 | 'Source', 5 | 'Media', 6 | 'Audio', 7 | 'UL', 8 | 'Track', 9 | 'Title', 10 | 'TextArea', 11 | 'Template', 12 | 'TBody', 13 | 'THead', 14 | 'TFoot', 15 | 'TR', 16 | 'Table', 17 | 'Col', 18 | 'ColGroup', 19 | 'TH', 20 | 'TD', 21 | 'Caption', 22 | 'Style', 23 | 'Span', 24 | 'Shadow', 25 | 'Select', 26 | 'Script', 27 | 'Quote', 28 | 'Progress', 29 | 'Pre', 30 | 'Picture', 31 | 'Param', 32 | 'P', 33 | 'Output', 34 | 'Option', 35 | 'Optgroup', 36 | 'Object', 37 | 'OL', 38 | 'Ins', 39 | 'Del', 40 | 'Meter', 41 | 'Meta', 42 | 'Menu', 43 | 'Map', 44 | 'Link', 45 | 'Legend', 46 | 'Label', 47 | 'LI', 48 | 'KeyGen', 49 | 'Input', 50 | 'Image', 51 | 'IFrame', 52 | 'H1', 53 | 'H2', 54 | 'H3', 55 | 'H4', 56 | 'H5', 57 | 'H6', 58 | 'Hr', 59 | 'FrameSet', 60 | 'Frame', 61 | 'Form', 62 | 'Font', 63 | 'Embed', 64 | 'Article', 65 | 'Aside', 66 | 'Footer', 67 | 'Figure', 68 | 'FigCaption', 69 | 'Header', 70 | 'Main', 71 | 'Mark', 72 | 'MenuItem', 73 | 'Nav', 74 | 'Section', 75 | 'Summary', 76 | 'WBr', 77 | 'Div', 78 | 'Dialog', 79 | 'Details', 80 | 'DataList', 81 | 'DL', 82 | 'Canvas', 83 | 'Button', 84 | 'Base', 85 | 'Br', 86 | 'Area', 87 | 'A'] 88 | for (var i = 0, l = elements.length; i < l; i++) { 89 | var name = elements[i] 90 | var element = document.createElement(name) 91 | var keys = [] 92 | for (key in element) { 93 | if (element.__proto__.hasOwnProperty(key)) { 94 | var value = element[key] 95 | if (typeof value === 'string' || typeof value === 'number' || value === 'boolean') { 96 | keys.push(key) 97 | } 98 | } 99 | } 100 | console.log(name, keys) 101 | } 102 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alkali", 3 | "main": "dist/index.js" 4 | } 5 | -------------------------------------------------------------------------------- /extensions/async-hooks.js: -------------------------------------------------------------------------------- 1 | var async_hooks = require('async_hooks'); 2 | var Variable = require('../Variable') 3 | 4 | var contexts = [] 5 | //global.asyncEvents = [] 6 | var previousContext 7 | module.exports.enable = () => { 8 | const alkaliAsyncHook = async_hooks.createHook({ 9 | init(asyncId, type, triggerAsyncId, resource) { 10 | if (type === 'PROMISE') {// might also do Timeout 11 | if (resource.isChainedPromise) { 12 | let context = Variable.currentContext 13 | if (context) { 14 | contexts[asyncId] = context 15 | // global.asyncEvents[asyncId] = [type + 'init ' + new Error().stack, context] 16 | } 17 | } 18 | } 19 | }, 20 | before(asyncId) { 21 | previousContext = Variable.currentContext 22 | // we could potentially throw an error if there is an existing previousContext, since there should not be a global context 23 | Variable.currentContext = contexts[asyncId]; 24 | // (global.asyncEvents[asyncId] || (global.asyncEvents[asyncId] = [])).push('before', previousContext) 25 | }, 26 | after(asyncId) { 27 | Variable.currentContext = undefined 28 | delete contexts[asyncId] 29 | // delete global.asyncEvents[asyncId] 30 | }, 31 | destroy(asyncId) { 32 | delete contexts[asyncId] 33 | // delete global.asyncEvents[asyncId] 34 | } 35 | }) 36 | alkaliAsyncHook.enable() 37 | } 38 | -------------------------------------------------------------------------------- /extensions/dstore.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 2 | define(['../util/lang', '../Variable'], factory) } else if (typeof module === 'object' && module.exports) { 3 | module.exports = factory(require('../util/lang'), require('../Variable')) // Node 4 | }}(this, function (lang, VariableExports) { 5 | var Variable = VariableExports.Variable 6 | var DstoreVariable = lang.compose(Variable, function DstoreVariable(value){ 7 | this.value = value 8 | }, 9 | { 10 | gotValue: function(value, context) { 11 | value = Variable.prototype.gotValue.call(this, value, context) 12 | if (value && value.on && !(this._lastDSValue === value)){ 13 | // if it an object that can deliver notifications through `on` events, we listen for that (like dstore collections) 14 | var variable = this 15 | if (this._lastHandle) { 16 | this._lastHandle.remove() 17 | } 18 | var handle = this._lastHandle = value.on(['add','update','delete'], function(event) { 19 | event.visited = new Set() 20 | variable.updated(event) 21 | }) 22 | this._lastDSValue = value 23 | this.returnedVariable = { 24 | // remove the listener when we unsubscribe 25 | stopNotifies: function() { 26 | variable._lastDSValue = undefined 27 | handle.remove() 28 | }, 29 | notifies: function(){} 30 | } 31 | } 32 | return value 33 | }, 34 | forEach: function(callback) { 35 | this.valueOf().forEach(callback) 36 | } 37 | }) 38 | 39 | function VArrayDstore(varray) { 40 | return { 41 | fetchRange: function(options) { 42 | Promise.prototype.otherwise = Promise.prototype.catch // make otherwise available 43 | let promise = Promise.resolve(varray.slice(options.start, options.end)) 44 | promise.totalLength = varray.then(function(array) { 45 | return array.length 46 | }) 47 | return promise 48 | }, 49 | on: function(eventType, listener) { 50 | var eventTypes = eventType.split('. ') 51 | var subscriber = { 52 | updated: function(event) { 53 | if (event.type === 'entry') { 54 | if (eventType.includes('update')) { 55 | var value = event.value 56 | var index = varray.valueOf().indexOf(value) 57 | listener({ 58 | index: index, 59 | previousIndex: index, 60 | target: value 61 | }) 62 | } 63 | } 64 | if (event.type === 'spliced') { 65 | 66 | } 67 | } 68 | } 69 | varray.notifies(subscriber) 70 | return { 71 | remove: function() { 72 | varray.stopNotifies(subscriber) 73 | } 74 | } 75 | }, 76 | track: function() { 77 | return this 78 | }, 79 | sort: function(sortInfo) { 80 | var sortFunction 81 | if (typeof sortInfo === 'function') { 82 | 83 | } 84 | 85 | return new VArrayDstore(varray.sort(sortFunction)) 86 | }, 87 | getIdentity: function(object) { 88 | return object.getId() 89 | } 90 | } 91 | } 92 | 93 | return { 94 | DstoreVariable: DstoreVariable, 95 | VArrayDstore: VArrayDstore 96 | } 97 | })) 98 | -------------------------------------------------------------------------------- /extensions/typescript.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 2 | define(['../Variable'], factory) } else if (typeof module === 'object' && module.exports) { 3 | module.exports = factory(require('../Variable')) // Node 4 | }}(this, function (VariableExports) { 5 | 6 | return { 7 | reactive: function(target, key) { 8 | var Type = Reflect.getMetadata('design:type', target, key) 9 | if (!Type.notifies) { 10 | //if (Type === Array) {} 11 | Type = VariableExports.Variable 12 | } 13 | Object.defineProperty(target, key, { 14 | get: function() { 15 | var property = (this._properties || (this._properties = {}))[key] 16 | if (!property) { 17 | this._properties[key] = property = new Type() 18 | if (this.getValue) { 19 | property.key = key 20 | property.parent = this 21 | } 22 | } 23 | return property 24 | }, 25 | set: function(value) { 26 | var property = this[key] 27 | property.parent ? VariableExports._changeValue(property, null, 4, value) : property.put(value) 28 | }, 29 | enumerable: true 30 | }) 31 | } 32 | } 33 | })) 34 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace alkali { 2 | type KeyType = string | number 3 | interface Promise { 4 | then(callback?: (value: T) => U | Promise, errback?: (value: T) => U | Promise): Promise 5 | } 6 | 7 | export class UpdateEvent { 8 | visited: Set> 9 | version?: number 10 | type: ('replaced' | 'property' | 'added' | 'deleted' | 'entry' | 'spliced' | 'discovered') 11 | child?: UpdateEvent 12 | triggers?: any[] 13 | sources?: Set 14 | source: any 15 | } 16 | 17 | // support heterogenous inputs https://github.com/Microsoft/TypeScript/pull/26063 18 | type YieldedValue = T extends Variable ? U : T 19 | type Yield = { [P in keyof T]: YieldedValue } 20 | 21 | interface Subscription { 22 | unsubscribe(): void 23 | } 24 | 25 | export class Variable implements Promise { 26 | /** 27 | * Create a new Variable, with reactive capabilities, that holds a varying value. 28 | * @param value Initial value for variable 29 | */ 30 | constructor(value?: T | Promise | Variable) 31 | /** 32 | * Gets the current value of the variable. 33 | * Note that this always returns synchronously, and if the variable has been provided a promise that has not resolved yet, will return last value 34 | */ 35 | valueOf(): T 36 | /** 37 | * Listen for the value of the variable, waiting if necessary, for any dependent promises to resolve. If the variable has a synchronously available value, the callback will be called immediately/synchronously 38 | */ 39 | then(callback?: (value: T) => U | Promise, errback?: (value: T) => U | Promise): Promise 40 | /** 41 | * Returns a variable corresponding to the property of the value of this variable 42 | * @param key The name of the property 43 | */ 44 | property(key: K): Variable> 45 | property(key: KeyType, PropertyClass: { new(): U }): U 46 | /** 47 | * Assigns a new value to this variables (which marks it as updated and any listeners will be notified). This is a request to change the variable, and subclasses can reject the put request, or return asynchronously. 48 | * @param value The new value to assign to the variable. The may be another variable or a promise, which will transitively be resolved 49 | */ 50 | put(value: T | Variable | Promise, event?: UpdateEvent): T | Variable | Promise 51 | /** 52 | * Gets the property value of this variable's value/object. This differs from `property` in that it returns the resolved value, not a variable 53 | * @param key The name of the property 54 | */ 55 | get(key: K): YieldedValue 56 | /** 57 | * Assigns a value to the property of this variable's value 58 | * @param key The name of the property 59 | * @param value The new value to assign to the property 60 | */ 61 | set(key: K, value: T[K] | Variable | Promise, event?: UpdateEvent): void 62 | /** 63 | * Assigns undefined to the property of this variable's value 64 | * @param key The name of the property 65 | */ 66 | undefine(key: KeyType): void 67 | /** 68 | * Define the value of this variable. This can be used to indicate that the some 69 | * @param variable The variable to proxy 70 | */ 71 | is(variable: Variable): this 72 | for(subject: any): this 73 | /** 74 | * Creates a new variable that is a transform of this variable, using the provided function to compute 75 | * the value of the new variable based on this variable's value. The returned variable is reactively dependent 76 | * on this variable. Note that this is computed lazily/as-needed, the transform function is not guaranteed to 77 | * execute on every change, only as needed by consumers. 78 | * @param transform The transform function to use to compute the value of the returned variable 79 | * @param reverseTransform The reverse transform function that is called when a value is put/set into the returned transform variable 80 | */ 81 | to(transform: (value: T) => Variable | Promise | U, reverseTransform?: (transformed: U) => any): Variable 82 | /** 83 | * Indicate that the variable's value has changed (primarily used if the value has been mutated outside the alkali API, and alkali needs to be notified of the change) 84 | * @param event An event object can be provided that will be passed to all variables that are updated/invalidated by this event 85 | */ 86 | updated(event?: UpdateEvent): UpdateEvent 87 | /** 88 | * Listen to the variable, and receive notification of update events 89 | * @param listener The listener object that will be called with data events. This will be called synchronously/immediately as part of the event dispatching. 90 | */ 91 | notifies(listener: { updated: (event: UpdateEvent) => any }): void 92 | /** 93 | * Subscribe to the variable, calling the listener after changes to the variable's value. 94 | * @param listener The listener function that will be called after data changes. This will be called on the next micro-turn. 95 | */ 96 | subscribe(listener: (event: { value:() => T }) => any): Subscription 97 | /** 98 | * Subscribe to the variable, calling the `next` method after changes to the variable's value. 99 | * @param listener The listener function that will be called immediately/synchronously after data changes. 100 | */ 101 | subscribe(observable: { next: (value: T) => any}): Subscription 102 | /** 103 | * Cast the variable to the provided type 104 | * @param Type 105 | */ 106 | as(Type: { new(): U }): U 107 | /** 108 | * Returns a new variable that is sourced from `this` variable and when the source 109 | * returns asynchronously (an upstream promise), this will immediately return 110 | * the `valueUntilResolved` until the `this` variable is resolved (and which point 111 | * it will update and return that source value) 112 | * @param valueUntilResolved The value to return while waiting for the source value to resolve 113 | * @param useLastValue whether to use the last resolved value until the next resolution 114 | */ 115 | whileResolving(valueUntilResolved: U, useLastValue?: boolean): Variable 116 | 117 | /** 118 | * Compose a new variable based on the provided input variables. The returned variable will hold an array 119 | * with elements corresponding to the values of the input variables, and will update in response to changes 120 | * to any of the input variables 121 | * @param inputs input variables 122 | */ 123 | static all(inputs: T): Variable> 124 | static all(inputs: T, transform: (...v: Yield) => U): Variable 125 | /** 126 | * Compose a new variable based on the provided input variables. The returned variable will hold an array 127 | * with elements corresponding to the values of the input variables, and will update in response to changes 128 | * to any of the input variables 129 | * @param inputs input variables 130 | */ 131 | static all(...inputs: T): Variable> 132 | 133 | static with(this: V, properties: {[P in keyof Props]: Props[P]}): { 134 | new (...args: any[]): V & Reacts 135 | } & V & Reacts 136 | 137 | static assign(properties: {[P in keyof U]: { new(): U[P] }}): VariableClass 138 | 139 | schema: Variable 140 | validation: Variable 141 | 142 | collection: VariableClass 143 | } 144 | export interface VariableClass { 145 | new(): Variable & T 146 | new(value?: T2): Variable & T 147 | (properties: {[P in keyof U]: { new (): U[P] }}): VariableClass 148 | with(properties: {[P in keyof U]: { new (): U[P] }}): VariableClass 149 | assign(properties: {[P in keyof U]: { new (): U[P] }}): VariableClass 150 | hasOwn(Target: () => any): void 151 | 152 | put(value: T | Variable | Promise): T | Variable | Promise 153 | valueOf(): T 154 | then(callback: (value: T) => U | Promise, errback: (value: T) => U | Promise): Promise 155 | for(subject: any): Variable 156 | to(transform: (value: T) => U | Variable): VariableClass 157 | property(key: K): VariableClass 158 | } 159 | 160 | export type Reacts = {[P in keyof T]?: Reacts} & Variable 161 | 162 | interface VC { 163 | (properties: T): T & U & VC 164 | new (properties: U): Variable 165 | } 166 | 167 | export class VArray extends Variable> implements Set { 168 | constructor(value?: Array | Promise> | Variable>) 169 | readonly length: typeof VNumber 170 | /** 171 | * Return a VArray with the map applied 172 | */ 173 | map(transform: (value: T) => U): VArray 174 | /** 175 | * Return a VArray with the filter applied 176 | */ 177 | filter(filterFunction: (value: T) => any): VArray 178 | /** 179 | * Iterate over the current value of the variable array 180 | */ 181 | //@ts-ignore 182 | forEach(each: (value: T, index: number, collection: Array) => {}) 183 | /** 184 | * Return a Variable with the reduce applied 185 | */ 186 | reduce(reducer: (memo: I, value: T, index: number) => U, initialValue?: I): Variable 187 | /** 188 | * Return a Variable with the reduceRight applied 189 | */ 190 | reduceRight(reducer: (memo: I, value: T, index: number) => U, initialValue?: I): Variable 191 | /** 192 | * Return a Variable with the some method applied 193 | */ 194 | some(filter: (value: T) => any): Variable 195 | /** 196 | * Return a Variable with the every method applied 197 | */ 198 | every(filter: (value: T) => any): Variable 199 | /** 200 | * Return a VArray with the slice applied 201 | */ 202 | slice(start: number, end?:number): Variable 203 | /** 204 | * Push a new value on to the array 205 | */ 206 | push(...items: any[]): number 207 | unshift(...items: any[]): number 208 | pop(): T 209 | shift(): T 210 | splice(start: number, end: number, ...items: any[]): T[] 211 | 212 | remove(item: T): void 213 | //@ts-ignore 214 | add(value: T): void 215 | //@ts-ignore 216 | delete(value: T): this 217 | has(value: T): boolean 218 | includes(searchElement: T): boolean 219 | 220 | static of: { 221 | /*>(collectionOf: { new (): U }): { new(v?: T[]): VArray } 222 | new>(collectionOf: { new (): U }): VArray*/ 223 | (collectionOf: { new (): U }): { new(v?: any[]): VArray } 224 | new(collectionOf: { new (): U }): VArray 225 | } 226 | entries(): IterableIterator<[T, T]> 227 | keys(): IterableIterator 228 | values(): IterableIterator 229 | clear(): void 230 | collectionOf: VariableClass 231 | [Symbol.toStringTag]: string 232 | [Symbol.iterator]: any 233 | } 234 | export class VMap extends Variable> { 235 | } 236 | export class VSet extends Variable> { 237 | } 238 | export class VPromise extends Variable> { 239 | //to(transform: (T) => VPromise | Variable | Promise | U): VPromise 240 | } 241 | export var VString: VariableClass 242 | export type vstring = string & Variable 243 | 244 | export var VNumber: VariableClass 245 | export type vnumber = number & Variable 246 | 247 | export class VBoolean extends Variable { 248 | } 249 | export class Item extends Variable { 250 | } 251 | export class Copy extends Variable { 252 | } 253 | export class Transform extends Variable { 254 | protected cachedValue: any 255 | protected cachedVersion: number 256 | constructor(source: any, transform: (...v: any[]) => T, sources?: any[]) 257 | } 258 | 259 | export function reactive(initialValue: string): Vstring 260 | export function reactive(initialValue: number): Vnumber 261 | export function reactive(initialValue: boolean): Boolean & VBoolean 262 | export function reactive(initialValue: T[]): VArray 263 | export function reactive(map: Map): VMap 264 | export function reactive(set: Set): VSet 265 | export function reactive(initialValue: T): Reacts 266 | export function reactive(): Variable 267 | 268 | /** 269 | * Decorator function to be used for marking properties, classes, methods as reactive 270 | */ 271 | export function reactive(target: {}, key?: string): void 272 | export function direct(target: {}, key?: string): void 273 | 274 | export function react(reactiveFunction: () => T): Variable 275 | /** 276 | * Compose a new variable based on the provided input variables. The returned variable will hold an array 277 | * with elements corresponding to the values of the input variables, and will update in response to changes 278 | * to any of the input variables 279 | * @param inputs input variables 280 | */ 281 | export function all(inputs: T): Variable> 282 | export function all(inputs: T, transform: (...v: Yield) => U): Variable 283 | /** 284 | * Compose a new variable based on the provided input variables. The returned variable will hold an array 285 | * with elements corresponding to the values of the input variables, and will update in response to changes 286 | * to any of the input variables 287 | * @param inputs input variables 288 | */ 289 | export function all(...inputs: T): Variable> 290 | 291 | /** 292 | * Execute the provided generator or generator iterator, resolving yielded promises and variables 293 | */ 294 | export function spawn(yieldingFunction: Iterator | (() => T)): Promise 295 | 296 | // operators 297 | export function not(variable: Variable | any): VBoolean 298 | export function add(a: Variable | number, b: Variable | number): vnumber 299 | export function subtract(a: Variable | number, b: Variable | number): vnumber 300 | export function multiply(a: Variable | number, b: Variable | number): vnumber 301 | export function divide(a: Variable | number, b: Variable | number): vnumber 302 | export function remainder(a: Variable | number): vnumber 303 | export function greater(a: Variable | number, b: Variable | number): VBoolean 304 | export function greaterOrEqual(a: Variable | number, b: Variable | number): VBoolean 305 | export function less(a: Variable | number, b: Variable | number): VBoolean 306 | export function lessOrEqual(a: Variable | number, b: Variable | number): VBoolean 307 | export function equal(a: Variable | any, b: Variable | any): VBoolean 308 | export function and(a: Variable | any, b: Variable | any): Variable 309 | export function or(a: Variable | any, b: Variable | any): Variable 310 | export function round(a: Variable | number): vnumber 311 | 312 | /* 313 | * The context used to compute a variable 314 | */ 315 | export class Context { 316 | /* A value that represents the entity requesting a value from a variable, allowing a variable 317 | * to adjust its computation based on who/what is requesting it */ 318 | subject: any 319 | /* The maximum version number of the sources used to compute the variable (usually accessed after a variable is computed to compare with future computations for differences). */ 320 | version: number 321 | setVersion(version: number): void 322 | constructor(subject?: any, notifies?: (receiver: any) => any) 323 | executeWithin(executor: () => T): T 324 | newContext(): Context 325 | } 326 | /* A response from a variable, given a context with an `ifModifiedSince` that indicates it has not changed*/ 327 | export var NOT_MODIFIED: {} 328 | /* 329 | * The current context being used to compute a variable. This primarily accessible from within a `valueOf` call. 330 | */ 331 | export var currentContext: Context 332 | interface RendererProperties { 333 | variable: Variable 334 | element: HTMLElement 335 | renderUpdate?: (value: T, element: HTMLElement) => void 336 | shouldRender?: (element: HTMLElement) => boolean 337 | alwaysUpdate?: boolean 338 | } 339 | interface NamedRendererProperties extends RendererProperties { 340 | name: string 341 | } 342 | export class Renderer { 343 | constructor(properties: RendererProperties) 344 | } 345 | export class ElementRenderer { 346 | constructor(properties: RendererProperties) 347 | } 348 | export class ContentRenderer { 349 | constructor(properties: RendererProperties) 350 | } 351 | export class AttributeRenderer { 352 | constructor(properties: NamedRendererProperties) 353 | } 354 | export class PropertyRenderer { 355 | constructor(properties: NamedRendererProperties) 356 | } 357 | export class StyleRenderer { 358 | constructor(properties: NamedRendererProperties) 359 | } 360 | export class TextRenderer { 361 | constructor(properties: RendererProperties) 362 | } 363 | interface ListRendererProperties extends RendererProperties { 364 | each: ElementChild | ((value: T, element: HTMLElement) => HTMLElement) 365 | } 366 | export class ListRenderer { 367 | constructor(properties: ListRendererProperties) 368 | } 369 | type Vstring = string | Variable 370 | type Vboolean = boolean | Variable 371 | type Vnumber = number | Variable 372 | type Vstyle = Vstring | Vnumber | Vboolean 373 | 374 | /** 375 | * @Deprecated Registers an element with the given tag name, returning a callable, newable constructor for the element 376 | **/ 377 | export function defineElement(tagSelect: string, Element: { new(...params: {}[]): T}): ElementClass 378 | /** 379 | * @Deprecated Returns a callable, newable constructor for the element 380 | **/ 381 | export function defineElement(Element: { new(...params: {}[]): T}): ElementClass 382 | 383 | export type ElementProperties = { 384 | [P in keyof T]?: T[P] 385 | } & BaseElementProperties 386 | 387 | type OptionalElementProperties = { 388 | [P in keyof T]?: T[P] 389 | } | BaseElementProperties | { 390 | [key: string]: any 391 | } 392 | 393 | interface BaseElementProperties { 394 | content?: ElementChild 395 | class?: Vstring 396 | for?: Vstring 397 | role?: Vstring 398 | classes?: {} 399 | attributes?: {} 400 | render?: () => any 401 | created?: (properties: BaseElementProperties) => BaseElementProperties | void 402 | ready?: (properties: BaseElementProperties) => any 403 | 404 | textContent?: Vstring 405 | 406 | msContentZoomFactor?: Vstring 407 | msRegionOverflow?: Vstring 408 | innerHTML?: Vstring 409 | onariarequest?: (ev: Event) => any 410 | oncommand?: (ev: Event) => any 411 | ongotpointercapture?: (ev: PointerEvent) => any 412 | onlostpointercapture?: (ev: PointerEvent) => any 413 | onmsgesturechange?: (ev: MSGestureEvent) => any 414 | onmsgesturedoubletap?: (ev: MSGestureEvent) => any 415 | onmsgestureend?: (ev: MSGestureEvent) => any 416 | onmsgesturehold?: (ev: MSGestureEvent) => any 417 | onmsgesturestart?: (ev: MSGestureEvent) => any 418 | onmsgesturetap?: (ev: MSGestureEvent) => any 419 | onmsgotpointercapture?: (ev: MSPointerEvent) => any 420 | onmsinertiastart?: (ev: MSGestureEvent) => any 421 | onmslostpointercapture?: (ev: MSPointerEvent) => any 422 | onmspointercancel?: (ev: MSPointerEvent) => any 423 | onmspointerdown?: (ev: MSPointerEvent) => any 424 | onmspointerenter?: (ev: MSPointerEvent) => any 425 | onmspointerleave?: (ev: MSPointerEvent) => any 426 | onmspointermove?: (ev: MSPointerEvent) => any 427 | onmspointerout?: (ev: MSPointerEvent) => any 428 | onmspointerover?: (ev: MSPointerEvent) => any 429 | onmspointerup?: (ev: MSPointerEvent) => any 430 | ontouchcancel?: (ev: TouchEvent) => any 431 | ontouchend?: (ev: TouchEvent) => any 432 | ontouchmove?: (ev: TouchEvent) => any 433 | ontouchstart?: (ev: TouchEvent) => any 434 | onwebkitfullscreenchange?: (ev: Event) => any 435 | onwebkitfullscreenerror?: (ev: Event) => any 436 | 437 | accessKey?: Vstring 438 | contentEditable?: Vstring 439 | dataset?: {} 440 | dir?: Vstring 441 | draggable?: Vboolean 442 | hidden?: Vboolean 443 | hideFocus?: Vboolean 444 | lang?: Vstring 445 | onabort?: (ev: Event) => any 446 | onactivate?: (ev: UIEvent) => any 447 | onbeforeactivate?: (ev: UIEvent) => any 448 | onbeforecopy?: (ev: DragEvent) => any 449 | onbeforecut?: (ev: DragEvent) => any 450 | onbeforedeactivate?: (ev: UIEvent) => any 451 | onbeforepaste?: (ev: DragEvent) => any 452 | onblur?: (ev: FocusEvent) => any 453 | oncanplay?: (ev: Event) => any 454 | oncanplaythrough?: (ev: Event) => any 455 | onchange?: (ev: Event) => any 456 | onclick?: (ev: MouseEvent) => any 457 | oncontextmenu?: (ev: PointerEvent) => any 458 | oncopy?: (ev: DragEvent) => any 459 | oncuechange?: (ev: Event) => any 460 | oncut?: (ev: DragEvent) => any 461 | ondblclick?: (ev: MouseEvent) => any 462 | ondeactivate?: (ev: UIEvent) => any 463 | ondrag?: (ev: DragEvent) => any 464 | ondragend?: (ev: DragEvent) => any 465 | ondragenter?: (ev: DragEvent) => any 466 | ondragleave?: (ev: DragEvent) => any 467 | ondragover?: (ev: DragEvent) => any 468 | ondragstart?: (ev: DragEvent) => any 469 | ondrop?: (ev: DragEvent) => any 470 | ondurationchange?: (ev: Event) => any 471 | onemptied?: (ev: Event) => any 472 | onended?: (ev: Event) => any 473 | onerror?: (ev: Event) => any 474 | onfocus?: (ev: FocusEvent) => any 475 | oninput?: (ev: Event) => any 476 | onkeydown?: (ev: KeyboardEvent) => any 477 | onkeypress?: (ev: KeyboardEvent) => any 478 | onkeyup?: (ev: KeyboardEvent) => any 479 | onload?: (ev: Event) => any 480 | onloadeddata?: (ev: Event) => any 481 | onloadedmetadata?: (ev: Event) => any 482 | onloadstart?: (ev: Event) => any 483 | onmousedown?: (ev: MouseEvent) => any 484 | onmouseenter?: (ev: MouseEvent) => any 485 | onmouseleave?: (ev: MouseEvent) => any 486 | onmousemove?: (ev: MouseEvent) => any 487 | onmouseout?: (ev: MouseEvent) => any 488 | onmouseover?: (ev: MouseEvent) => any 489 | onmouseup?: (ev: MouseEvent) => any 490 | onmousewheel?: (ev: MouseWheelEvent) => any 491 | onmscontentzoom?: (ev: UIEvent) => any 492 | onpaste?: (ev: DragEvent) => any 493 | onpause?: (ev: Event) => any 494 | onplay?: (ev: Event) => any 495 | onplaying?: (ev: Event) => any 496 | onprogress?: (ev: ProgressEvent) => any 497 | onratechange?: (ev: Event) => any 498 | onreset?: (ev: Event) => any 499 | onscroll?: (ev: UIEvent) => any 500 | onseeked?: (ev: Event) => any 501 | onseeking?: (ev: Event) => any 502 | onselect?: (ev: UIEvent) => any 503 | onselectstart?: (ev: Event) => any 504 | onstalled?: (ev: Event) => any 505 | onsubmit?: (ev: Event) => any 506 | onsuspend?: (ev: Event) => any 507 | ontimeupdate?: (ev: Event) => any 508 | onvolumechange?: (ev: Event) => any 509 | onwaiting?: (ev: Event) => any 510 | spellcheck?: Vboolean 511 | style?: {} 512 | tabIndex?: Vnumber 513 | title?: Vstring 514 | 515 | align?: Vstring 516 | noWrap?: Vboolean 517 | disabled?: Vboolean 518 | href?: Vstring 519 | src?: Vstring 520 | 521 | alignContent?: Vstyle 522 | alignItems?: Vstyle 523 | alignSelf?: Vstyle 524 | animation?: Vstyle 525 | animationDelay?: Vstyle 526 | animationDirection?: Vstyle 527 | animationDuration?: Vstyle 528 | animationFillMode?: Vstyle 529 | animationIterationCount?: Vstyle 530 | animationName?: Vstyle 531 | animationPlayState?: Vstyle 532 | animationTimingFunction?: Vstyle 533 | backfaceVisibility?: Vstyle 534 | background?: Vstyle 535 | backgroundAttachment?: Vstyle 536 | backgroundBlendMode?: Vstyle 537 | backgroundClip?: Vstyle 538 | backgroundColor?: Vstyle 539 | backgroundImage?: Vstyle 540 | backgroundOrigin?: Vstyle 541 | backgroundPosition?: Vstyle 542 | backgroundPositionX?: Vstyle 543 | backgroundPositionY?: Vstyle 544 | backgroundRepeat?: Vstyle 545 | backgroundRepeatX?: Vstyle 546 | backgroundRepeatY?: Vstyle 547 | backgroundSize?: Vstyle 548 | baselineShift?: Vstyle 549 | border?: Vstyle 550 | borderBottom?: Vstyle 551 | borderBottomColor?: Vstyle 552 | borderBottomLeftRadius?: Vstyle 553 | borderBottomRightRadius?: Vstyle 554 | borderBottomStyle?: Vstyle 555 | borderBottomWidth?: Vstyle 556 | borderCollapse?: Vstyle 557 | borderColor?: Vstyle 558 | borderImage?: Vstyle 559 | borderImageOutset?: Vstyle 560 | borderImageRepeat?: Vstyle 561 | borderImageSlice?: Vstyle 562 | borderImageSource?: Vstyle 563 | borderImageWidth?: Vstyle 564 | borderLeft?: Vstyle 565 | borderLeftColor?: Vstyle 566 | borderLeftStyle?: Vstyle 567 | borderLeftWidth?: Vstyle 568 | borderRadius?: Vstyle 569 | borderRight?: Vstyle 570 | borderRightColor?: Vstyle 571 | borderRightStyle?: Vstyle 572 | borderRightWidth?: Vstyle 573 | borderSpacing?: Vstyle 574 | borderStyle?: Vstyle 575 | borderTop?: Vstyle 576 | borderTopColor?: Vstyle 577 | borderTopLeftRadius?: Vstyle 578 | borderTopRightRadius?: Vstyle 579 | borderTopStyle?: Vstyle 580 | borderTopWidth?: Vstyle 581 | borderWidth?: Vstyle 582 | bottom?: Vstyle 583 | boxShadow?: Vstyle 584 | boxSizing?: Vstyle 585 | bufferedRendering?: Vstyle 586 | captionSide?: Vstyle 587 | clear?: Vstyle 588 | clip?: Vstyle 589 | clipPath?: Vstyle 590 | clipRule?: Vstyle 591 | color?: Vstyle 592 | colorInterpolation?: Vstyle 593 | colorInterpolationFilters?: Vstyle 594 | colorRendering?: Vstyle 595 | counterIncrement?: Vstyle 596 | counterReset?: Vstyle 597 | cursor?: Vstyle 598 | direction?: Vstyle 599 | display?: Vstyle 600 | emptyCells?: Vstyle 601 | fill?: Vstyle 602 | fillOpacity?: Vstyle 603 | fillRule?: Vstyle 604 | filter?: Vstyle 605 | flex?: Vstyle 606 | flexBasis?: Vstyle 607 | flexDirection?: Vstyle 608 | flexFlow?: Vstyle 609 | flexGrow?: Vstyle 610 | flexShrink?: Vstyle 611 | flexWrap?: Vstyle 612 | float?: Vstyle 613 | floodColor?: Vstyle 614 | floodOpacity?: Vstyle 615 | font?: Vstyle 616 | fontFamily?: Vstyle 617 | fontFeatureSettings?: Vstyle 618 | fontKerning?: Vstyle 619 | fontSize?: Vstyle 620 | fontStretch?: Vstyle 621 | fontStyle?: Vstyle 622 | fontVariant?: Vstyle 623 | fontVariantLigatures?: Vstyle 624 | fontWeight?: Vstyle 625 | height?: Vstyle 626 | imageRendering?: Vstyle 627 | isolation?: Vstyle 628 | justifyContent?: Vstyle 629 | left?: Vstyle 630 | letterSpacing?: Vstyle 631 | lightingColor?: Vstyle 632 | lineHeight?: Vstyle 633 | listStyle?: Vstyle 634 | listStyleImage?: Vstyle 635 | listStylePosition?: Vstyle 636 | listStyleType?: Vstyle 637 | margin?: Vstyle 638 | marginBottom?: Vstyle 639 | marginLeft?: Vstyle 640 | marginRight?: Vstyle 641 | marginTop?: Vstyle 642 | marker?: Vstyle 643 | markerEnd?: Vstyle 644 | markerMid?: Vstyle 645 | markerStart?: Vstyle 646 | mask?: Vstyle 647 | maskType?: Vstyle 648 | maxHeight?: Vstyle 649 | maxWidth?: Vstyle 650 | maxZoom?: Vstyle 651 | minHeight?: Vstyle 652 | minWidth?: Vstyle 653 | minZoom?: Vstyle 654 | mixBlendMode?: Vstyle 655 | motion?: Vstyle 656 | motionOffset?: Vstyle 657 | motionPath?: Vstyle 658 | motionRotation?: Vstyle 659 | objectFit?: Vstyle 660 | objectPosition?: Vstyle 661 | opacity?: Vstyle 662 | order?: Vstyle 663 | orientation?: Vstyle 664 | orphans?: Vstyle 665 | outline?: Vstyle 666 | outlineColor?: Vstyle 667 | outlineOffset?: Vstyle 668 | outlineStyle?: Vstyle 669 | outlineWidth?: Vstyle 670 | overflow?: Vstyle 671 | overflowWrap?: Vstyle 672 | overflowX?: Vstyle 673 | overflowY?: Vstyle 674 | padding?: Vstyle 675 | paddingBottom?: Vstyle 676 | paddingLeft?: Vstyle 677 | paddingRight?: Vstyle 678 | paddingTop?: Vstyle 679 | page?: Vstyle 680 | pageBreakAfter?: Vstyle 681 | pageBreakBefore?: Vstyle 682 | pageBreakInside?: Vstyle 683 | paintOrder?: Vstyle 684 | perspective?: Vstyle 685 | perspectiveOrigin?: Vstyle 686 | pointerEvents?: Vstyle 687 | position?: Vstyle 688 | quotes?: Vstyle 689 | resize?: Vstyle 690 | right?: Vstyle 691 | shapeImageThreshold?: Vstyle 692 | shapeMargin?: Vstyle 693 | shapeOutside?: Vstyle 694 | shapeRendering?: Vstyle 695 | size?: Vstyle 696 | speak?: Vstyle 697 | stopColor?: Vstyle 698 | stopOpacity?: Vstyle 699 | stroke?: Vstyle 700 | strokeDasharray?: Vstyle 701 | strokeDashoffset?: Vstyle 702 | strokeLinecap?: Vstyle 703 | strokeLinejoin?: Vstyle 704 | strokeMiterlimit?: Vstyle 705 | strokeOpacity?: Vstyle 706 | strokeWidth?: Vstyle 707 | tabSize?: Vstyle 708 | tableLayout?: Vstyle 709 | textAlign?: Vstyle 710 | textAlignLast?: Vstyle 711 | textAnchor?: Vstyle 712 | textCombineUpright?: Vstyle 713 | textDecoration?: Vstyle 714 | textIndent?: Vstyle 715 | textOrientation?: Vstyle 716 | textOverflow?: Vstyle 717 | textRendering?: Vstyle 718 | textShadow?: Vstyle 719 | textTransform?: Vstyle 720 | top?: Vstyle 721 | touchAction?: Vstyle 722 | transform?: Vstyle 723 | transformOrigin?: Vstyle 724 | transformStyle?: Vstyle 725 | transition?: Vstyle 726 | transitionDelay?: Vstyle 727 | transitionDuration?: Vstyle 728 | transitionProperty?: Vstyle 729 | transitionTimingFunction?: Vstyle 730 | unicodeBidi?: Vstyle 731 | unicodeRange?: Vstyle 732 | userZoom?: Vstyle 733 | vectorEffect?: Vstyle 734 | verticalAlign?: Vstyle 735 | visibility?: Vstyle 736 | whiteSpace?: Vstyle 737 | widows?: Vstyle 738 | width?: Vstyle 739 | willChange?: Vstyle 740 | wordBreak?: Vstyle 741 | wordSpacing?: Vstyle 742 | wordWrap?: Vstyle 743 | writingMode?: Vstyle 744 | zIndex?: Vstyle 745 | zoom?: Vstyle 746 | [name: string]: any 747 | } 748 | 749 | type ElementChild = string | Variable | ElementClass | VariableClass | Array | Node | number | boolean 750 | type ElementChild2 = string | Variable | ElementClass | VariableClass | Array | Node | number | boolean 751 | type ElementChild3 = string | Variable | ElementClass | VariableClass | Array | Node | number | boolean 752 | 753 | export interface ElementClass { 754 | new (properties: OptionalElementProperties | {[P in keyof T]: T[P]}, content?: ElementChild): Element & T 755 | new (properties: OptionalElementProperties, content?: ElementChild): Element 756 | new (selector?: string): Element 757 | new (content: ElementChild): Element 758 | new (selector: string, content: ElementChild): Element 759 | new (selector: string, properties: OptionalElementProperties | {[P in keyof T]: T[P]}, content?: ElementChild): Element & T 760 | new (selector: string, properties: OptionalElementProperties, content?: ElementChild): Element 761 | (selector?: string): ElementClass 762 | (content: ElementChild): ElementClass 763 | (properties: OptionalElementProperties | {[P in keyof T]: T[P]}, content?: ElementChild): ElementClass 764 | (selector: string, content: ElementChild, properties?: OptionalElementProperties): ElementClass 765 | (selector: string, properties: OptionalElementProperties | {[P in keyof T]: T[P]}, content?: ElementChild): ElementClass 766 | create(selector?: string): Element 767 | create(content: ElementChild): Element 768 | create(properties: OptionalElementProperties, content?: ElementChild): Element 769 | create(selector: string, content: ElementChild): Element 770 | create(selector: string, properties: OptionalElementProperties, content?: ElementChild): Element 771 | with(selector: string, properties: OptionalElementProperties | {[P in keyof T]: T[P]}, content?: ElementChild): ElementClass 772 | with(properties: OptionalElementProperties | {[P in keyof T]: T[P]}, content?: ElementChild): ElementClass 773 | with(selector: string): ElementClass 774 | defineElement(this: { new(...params: {}[]): T}, tagSelect?: string): ElementClass 775 | property(key: KeyType): VariableClass 776 | children: Array 777 | } 778 | export var Element: ElementClass 779 | 780 | export var Video: ElementClass 781 | export var Source: ElementClass 782 | export var Media: ElementClass 783 | export var Audio: ElementClass 784 | export var UL: ElementClass 785 | export var Track: ElementClass 786 | export var Title: ElementClass 787 | export var TextArea: ElementClass 788 | export var Template: ElementClass 789 | export var TBody: ElementClass 790 | export var THead: ElementClass 791 | export var TFoot: ElementClass 792 | export var TR: ElementClass 793 | export var Table: ElementClass 794 | export var Col: ElementClass 795 | export var ColGroup: ElementClass 796 | export var TH: ElementClass 797 | export var TD: ElementClass 798 | export var Caption: ElementClass 799 | export var Style: ElementClass 800 | export var Span: ElementClass 801 | export var Shadow: ElementClass 802 | export var Select: ElementClass 803 | export var Script: ElementClass 804 | export var Quote: ElementClass 805 | export var Progress: ElementClass 806 | export var Pre: ElementClass 807 | export var Picture: ElementClass 808 | export var Param: ElementClass 809 | export var P: ElementClass 810 | export var Output: ElementClass 811 | export var Option: ElementClass 812 | export var OptGroup: ElementClass 813 | export var Object: ElementClass 814 | export var OL: ElementClass 815 | export var Ins: ElementClass 816 | export var Del: ElementClass 817 | export var Meter: ElementClass 818 | export var Meta: ElementClass 819 | export var Menu: ElementClass 820 | export var Map: ElementClass 821 | export var Link: ElementClass 822 | export var Legend: ElementClass 823 | export var Label: ElementClass 824 | export var LI: ElementClass 825 | export var KeyGen: ElementClass 826 | export var Image: ElementClass 827 | export var Img: ElementClass 828 | export var IFrame: ElementClass 829 | export var H1: ElementClass 830 | export var H2: ElementClass 831 | export var H3: ElementClass 832 | export var H4: ElementClass 833 | export var H5: ElementClass 834 | export var H6: ElementClass 835 | export var Hr: ElementClass 836 | export var FrameSet: ElementClass 837 | export var Frame: ElementClass 838 | export var Form: ElementClass 839 | export var Font: ElementClass 840 | export var Embed: ElementClass 841 | export var Em: ElementClass 842 | export var Code: ElementClass 843 | export var Article: ElementClass 844 | export var Aside: ElementClass 845 | export var Figure: ElementClass 846 | export var FigCaption: ElementClass 847 | export var Header: ElementClass 848 | export var Main: ElementClass 849 | export var Mark: ElementClass 850 | export var MenuItem: ElementClass 851 | export var Nav: ElementClass 852 | export var Section: ElementClass 853 | export var Summary: ElementClass 854 | export var WBr: ElementClass 855 | export var Div: ElementClass 856 | export var Dialog: ElementClass 857 | export var Details: ElementClass 858 | export var DataList: ElementClass 859 | export var DL: ElementClass 860 | export var DT: ElementClass 861 | export var DD: ElementClass 862 | export var Canvas: ElementClass 863 | export var Button: ElementClass 864 | export var Base: ElementClass 865 | export var Br: ElementClass 866 | export var Area: ElementClass 867 | export var A: ElementClass 868 | export var B: ElementClass 869 | export var I: ElementClass 870 | 871 | export var Anchor: ElementClass 872 | export var Paragraph: ElementClass 873 | export var DList: ElementClass 874 | export var UList: ElementClass 875 | export var OList: ElementClass 876 | export var ListItem: ElementClass 877 | export var Input: ElementClass 878 | export var TableRow: ElementClass 879 | export var TableCell: ElementClass 880 | export var TableHeaderCell: ElementClass 881 | export var TableHeader: ElementClass 882 | export var TableBody: ElementClass 883 | 884 | export var Checkbox: ElementClass 885 | export var CheckboxInput: ElementClass 886 | export var Password: ElementClass 887 | export var PasswordInput: ElementClass 888 | export var Text: ElementClass 889 | export var TextInput: ElementClass 890 | export var Submit: ElementClass 891 | export var SubmitInput: ElementClass 892 | export var Radio: ElementClass 893 | export var RadioInput: ElementClass 894 | export var Color: ElementClass 895 | export var ColorInput: ElementClass 896 | export var Date: ElementClass 897 | export var DateInput: ElementClass 898 | export var DateTime: ElementClass 899 | export var DateTimeInput: ElementClass 900 | export var Email: ElementClass 901 | export var EmailInput: ElementClass 902 | export var Month: ElementClass 903 | export var MonthInput: ElementClass 904 | export var Number: ElementClass 905 | export var NumberInput: ElementClass 906 | export var Range: ElementClass 907 | export var RangeInput: ElementClass 908 | export var Search: ElementClass 909 | export var SearchInput: ElementClass 910 | export var Tel: ElementClass 911 | export var TelInput: ElementClass 912 | export var Time: ElementClass 913 | export var TimeInput: ElementClass 914 | export var Url: ElementClass 915 | export var UrlInput: ElementClass 916 | export var Week: ElementClass 917 | export var WeekInput: ElementClass 918 | 919 | export function assign(element: HTMLElement, properties: ElementProperties): void 920 | export function append(...args: Array): HTMLElement 921 | export function prepend(...args: Array): HTMLElement 922 | export function onShowElement(element: Node): void 923 | export function onElementRemoval(element: Node, onlyChildren?: boolean): void 924 | export function content(node: T): T 925 | export function createElement(element: string|Function, properties: ElementProperties, ...children: Array): ElementClass 926 | 927 | export function getNextVersion(): number 928 | 929 | export class ReplacedEvent extends UpdateEvent {} 930 | export class EntryEvent extends UpdateEvent {} 931 | export class AddedEvent extends UpdateEvent {} 932 | export class DeletedEvent extends UpdateEvent {} 933 | export class ContextualPromise extends Promise { 934 | constructor(executor: Function) 935 | } 936 | } 937 | declare module 'alkali' { 938 | export = alkali 939 | } 940 | 941 | declare module 'alkali/extensions/typescript' { 942 | export function reactive(target: any, key: string): void 943 | } 944 | 945 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /// 2 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 3 | define(['./Element', './Renderer', './reactive', './Copy', './operators', './Variable', './util/lang', './util/ContextualPromise'], factory) } else if (typeof module === 'object' && module.exports) { 4 | module.exports = factory(require('./Element'), require('./Renderer'), require('./reactive'), require('./Copy'), require('./operators'), require('./Variable'), require('./util/lang'), require('./util/ContextualPromise')) // Node 5 | }}(this, function (Element, Renderer, reactive, Copy, operators, VariableExports, lang, ContextualPromise) { 6 | 7 | var main = Object.create(Element) 8 | main.Copy = Copy 9 | main.Element = Element 10 | lang.copy(main, VariableExports) 11 | Object.defineProperty(main, 'currentContext', Object.getOwnPropertyDescriptor(VariableExports, 'currentContext')) 12 | main.reactive = reactive 13 | lang.copy(main.react, reactive) // For backwards compatibility with babel transform 14 | main.spawn = lang.spawn 15 | main.when = lang.when 16 | main.Renderer = Renderer.ElementRenderer 17 | lang.copy(main, Renderer) 18 | lang.copy(main, operators) 19 | main.ContextualPromise = ContextualPromise 20 | main.default = reactive // default export 21 | var localGlobal = typeof window == 'undefined' ? global : window 22 | if (localGlobal.alkali) { 23 | console.warn('Multiple instances of alkali have been defined, which can cause alkali context instances to be out of sync') 24 | } 25 | return localGlobal.alkali = main 26 | })) 27 | -------------------------------------------------------------------------------- /operators.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 2 | define(['./Variable'], factory) } else if (typeof module === 'object' && module.exports) { 3 | module.exports = factory(require('./Variable')) // Node 4 | }}(this, function (VariableExports) { 5 | 6 | var VBoolean = VariableExports.VBoolean 7 | var VNumber = VariableExports.VNumber 8 | var operatingFunctions = {}; 9 | var operators = {}; 10 | function getOperatingFunction(expression){ 11 | // jshint evil: true 12 | return operatingFunctions[expression] || 13 | (operatingFunctions[expression] = 14 | new Function('a', 'b', 'return ' + expression)); 15 | } 16 | function operator(operator, type, name, precedence, forward, reverseA, reverseB){ 17 | // defines the standard operators 18 | var reverse = function(output, inputs){ 19 | var a = inputs[0], 20 | b = inputs[1] 21 | var firstError 22 | if(a && a.put){ 23 | try { 24 | return a.put(reverseA(output, b && b.valueOf())) 25 | } catch(error) { 26 | if (error.deniedPut) { 27 | firstError = error 28 | } else { 29 | throw error 30 | } 31 | } 32 | } 33 | if(b && b.put){ 34 | b.put(reverseB(output, a && a.valueOf())) 35 | } else { 36 | throw (firstError && firstError.message ? firstError : new Error('Can not assign change value to constant operators')) 37 | } 38 | }; 39 | // define a function that can lazily ensure the operating function 40 | // is available 41 | var operatorHandler = { 42 | apply: function(instance, args){ 43 | forward = getOperatingFunction(forward); 44 | reverseA = reverseA && getOperatingFunction(reverseA); 45 | reverseB = reverseB && getOperatingFunction(reverseB); 46 | forward.reverse = reverse; 47 | operators[operator] = operatorHandler = new VariableExports.Variable(forward); 48 | 49 | addFlags(operatorHandler); 50 | args = Array.prototype.slice.call(args); 51 | return operatorHandler.apply(instance, args); 52 | } 53 | }; 54 | function addFlags(operatorHandler){ 55 | operatorHandler.precedence = precedence; 56 | operatorHandler.infix = reverseB !== false; 57 | } 58 | addFlags(operatorHandler); 59 | operators[operator] = operatorHandler; 60 | operators[name] = function() { 61 | var result = operatorHandler.apply(null, arguments) 62 | return type ? result.as(type) : result 63 | } 64 | } 65 | // using order precedence from: 66 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence 67 | operator('+', VNumber, 'add', 6, 'a+b', 'a-b', 'a-b'); 68 | operator('-', VNumber, 'subtract', 6, 'a-b', 'a+b', 'b-a'); 69 | operator('*', VNumber, 'multiply', 5, 'a*b', 'a/b', 'a/b'); 70 | operator('/', VNumber, 'divide', 5, 'a/b', 'a*b', 'b/a'); 71 | // operator('^', 7, 'a^b', 'a^(-b)', 'Math.log(a)/Math.log(b)'); 72 | operator('?', null, 'if', 16, 'b[a?0:1]', 'a===b[0]||(a===b[1]?false:(function(){throw new Error()})())', '[a,b]'); 73 | operator(':', null, 'choose', 15, '[a,b]', 'a[0]?a[1]:(function(){throw new Error()})()', 'a[1]'); 74 | operator('!', VBoolean, 'not', 4, '!a', '!a', false); 75 | operator('%', VNumber, 'remainder', 5, 'a%b'); 76 | operator('>', VBoolean, 'greater', 8, 'a>b'); 77 | operator('>=', VBoolean, 'greaterOrEqual', 8, 'a>=b'); 78 | operator('<', VBoolean, 'less', 8, 'a -1) 595 | assert.isTrue(container.firstChild.firstChild.nextSibling.className.indexOf('hidden') > -1) 596 | assert.isFalse(container.firstChild.firstChild.nextSibling.nextSibling.className.indexOf('hidden') > -1) 597 | assert.isFalse(container.firstChild.firstChild.nextSibling.nextSibling.nextSibling.className.indexOf('hidden') > -1) 598 | }) 599 | }) 600 | 601 | test('inheritance', function() { 602 | var a = new Variable('a') 603 | var b = new Variable('b') 604 | 605 | var MyDiv = Div({ 606 | textContent: a, 607 | title: a 608 | }) 609 | var div1 = new MyDiv({title: b}) 610 | document.body.appendChild(div1) 611 | assert.strictEqual(div1.textContent, 'a') 612 | assert.strictEqual(div1.title, 'b') 613 | a.put('A') 614 | return new Promise(requestAnimationFrame).then(function(){ 615 | assert.strictEqual(div1.textContent, 'A') 616 | assert.strictEqual(div1.title, 'b') 617 | b.put('B') 618 | return new Promise(requestAnimationFrame).then(function(){ 619 | assert.strictEqual(div1.textContent, 'A') 620 | assert.strictEqual(div1.title, 'B') 621 | }) 622 | }) 623 | }) 624 | 625 | test('structured elements', function() { 626 | var Name = VString() 627 | var MyDiv = Div({ 628 | name: Name, 629 | textContent: Name.to(function(name) { return name.slice(0, 10) }), 630 | title: Name 631 | }) 632 | var div1 = new MyDiv({ name: 'make sure this sliced' }) 633 | document.body.appendChild(div1) 634 | assert.strictEqual(div1.textContent, 'make sure ') 635 | assert.strictEqual(div1.title, 'make sure this sliced') 636 | /* a.put('A') 637 | return new Promise(requestAnimationFrame).then(function(){ 638 | assert.strictEqual(div1.textContent, 'A') 639 | assert.strictEqual(div1.title, 'b') 640 | b.put('B') 641 | return new Promise(requestAnimationFrame).then(function(){ 642 | assert.strictEqual(div1.textContent, 'A') 643 | assert.strictEqual(div1.title, 'B') 644 | }) 645 | })*/ 646 | }) 647 | 648 | test('cleanup', function() { 649 | var a = new Variable('a') 650 | var div = new Div([ 651 | a, 652 | Span(a)]) 653 | document.body.appendChild(div) 654 | return new Promise(requestAnimationFrame).then(function() { 655 | assert.strictEqual(a.listeners.length, 2) 656 | document.body.removeChild(div) 657 | return new Promise(requestAnimationFrame).then(function() { 658 | assert.strictEqual(a.listeners, false) 659 | }) 660 | }) 661 | }) 662 | test('classes', function() { 663 | var a = new Variable(false) 664 | var b = new Variable(true) 665 | var DivWithClass = Div('.first') 666 | var div = new DivWithClass('.second.third', { 667 | classes: { 668 | a: a, 669 | b: b 670 | } 671 | }) 672 | var div2 = new Div('.one.two') 673 | var divWithoutInitialClass = new Div({ classes: { dynamicclass: b }}) 674 | document.body.appendChild(div) 675 | return new Promise(requestAnimationFrame).then(function() { 676 | assert.strictEqual(div2.className, 'one two') 677 | assert.strictEqual(div.className, 'first second third b') 678 | assert.strictEqual(divWithoutInitialClass.className, 'dynamicclass') 679 | a.put(true) 680 | return new Promise(requestAnimationFrame).then(function() { 681 | assert.strictEqual(div.className, 'first second third b a') 682 | b.put(false) 683 | return new Promise(requestAnimationFrame).then(function() { 684 | assert.strictEqual(div.className, 'first second third a') 685 | a.put(false) 686 | return new Promise(requestAnimationFrame).then(function() { 687 | assert.strictEqual(div.className, 'first second third') 688 | }) 689 | }) 690 | }) 691 | }) 692 | }) 693 | test('classesAsObjectVariable', function() { 694 | var obj = new Variable({ 695 | b: true 696 | }) 697 | var DivWithClass = Div('.first') 698 | var div = new DivWithClass('.second.third', { 699 | classes: obj 700 | }) 701 | document.body.appendChild(div) 702 | return new Promise(requestAnimationFrame).then(function() { 703 | assert.strictEqual(div.className, 'first second third b') 704 | obj.set('a', true) 705 | return new Promise(requestAnimationFrame).then(function() { 706 | assert.strictEqual(div.className, 'first second third b a') 707 | obj.put({ a: true }) 708 | return new Promise(requestAnimationFrame).then(function() { 709 | assert.strictEqual(div.className, 'first second third a') 710 | obj.put({ b: false }) 711 | return new Promise(requestAnimationFrame).then(function() { 712 | assert.strictEqual(div.className, 'first second third') 713 | }) 714 | }) 715 | }) 716 | }) 717 | }) 718 | test('dynamicClass', function() { 719 | var MyButton = Div({ 720 | num: Variable 721 | }) 722 | MyButton.children = [ 723 | Div({ 724 | 'class': MyButton.num.to(function(num) { 725 | return 'test-' + num 726 | }) 727 | })] 728 | var b = new MyButton({num: 2}) 729 | assert.strictEqual(b.firstChild.className, 'test-2') 730 | }) 731 | test('renderProperty', function() { 732 | var MyComponent = extend(Div, { 733 | _renderFoo: function(value) { 734 | this.textContent = value + 'foo' 735 | } 736 | }) 737 | var foo = new Variable(1) 738 | var div = new MyComponent({ 739 | foo: foo 740 | }) 741 | document.body.appendChild(div) 742 | return new Promise(requestAnimationFrame).then(function() { 743 | assert.strictEqual(div.textContent, '1foo') 744 | foo.put(2) 745 | return new Promise(requestAnimationFrame).then(function() { 746 | assert.strictEqual(div.textContent, '2foo') 747 | div.foo = 3 748 | return new Promise(requestAnimationFrame).then(function() { 749 | assert.strictEqual(div.textContent, '3foo') 750 | }) 751 | }) 752 | }) 753 | }) 754 | test('attributes', function() { 755 | var v = new Variable('one') 756 | var div = new Div({ 757 | attributes: { 758 | role: 'button', 759 | 'aria-describedby': v 760 | } 761 | }) 762 | document.body.appendChild(div) 763 | assert.strictEqual(div.getAttribute('role'), 'button') 764 | assert.strictEqual(div.getAttribute('aria-describedby'), 'one') 765 | v.put('two') 766 | return new Promise(requestAnimationFrame).then(function() { 767 | assert.strictEqual(div.getAttribute('aria-describedby'), 'two') 768 | }) 769 | }) 770 | test('attributesWithObject', function() { 771 | var v = new Variable({ 772 | role: 'button', 773 | 'aria-describedby': 'one' 774 | }) 775 | var div = new Div({ 776 | attributes: v 777 | }) 778 | document.body.appendChild(div) 779 | assert.strictEqual(div.getAttribute('role'), 'button') 780 | assert.strictEqual(div.getAttribute('aria-describedby'), 'one') 781 | v.put({ 782 | 'aria-describedby': 'two' 783 | }) 784 | return new Promise(requestAnimationFrame).then(function() { 785 | assert.strictEqual(div.getAttribute('aria-describedby'), 'two') 786 | assert.isFalse(div.hasAttribute('role')) 787 | }) 788 | }) 789 | test('dataset', function() { 790 | var v = new Variable('one') 791 | var div = new Div({ 792 | dataset: { 793 | foo: 'foo-value', 794 | bar: v 795 | } 796 | }) 797 | document.body.appendChild(div) 798 | assert.strictEqual(div.getAttribute('data-foo'), 'foo-value') 799 | assert.strictEqual(div.dataset.foo, 'foo-value') 800 | assert.strictEqual(div.getAttribute('data-bar'), 'one') 801 | v.put('two') 802 | return new Promise(requestAnimationFrame).then(function() { 803 | assert.strictEqual(div.getAttribute('data-bar'), 'two') 804 | }) 805 | }) 806 | test('datasetWithObject', function() { 807 | var v = new Variable({ 808 | foo: 'foo-value', 809 | bar: 'one' 810 | }) 811 | var div = new Div({ 812 | dataset: v 813 | }) 814 | document.body.appendChild(div) 815 | assert.strictEqual(div.getAttribute('data-foo'), 'foo-value') 816 | assert.strictEqual(div.dataset.foo, 'foo-value') 817 | assert.strictEqual(div.getAttribute('data-bar'), 'one') 818 | v.put({ 819 | bar: 'two' 820 | }) 821 | return new Promise(requestAnimationFrame).then(function() { 822 | assert.isFalse(div.hasAttribute('data-foo')) 823 | assert.strictEqual(div.dataset.foo, undefined) 824 | assert.strictEqual(div.getAttribute('data-bar'), 'two') 825 | }) 826 | }) 827 | test('styleObject', function() { 828 | var v = new Variable('25px') 829 | var MyDiv = Div({ 830 | style: { 831 | marginLeft: '10px', 832 | paddingLeft: '10px' 833 | } 834 | }) 835 | var MyDiv2 = MyDiv({ 836 | style: { 837 | paddingLeft: v 838 | } 839 | }) 840 | var div = new MyDiv2({ 841 | style: { 842 | display: 'inline-block' 843 | } 844 | }) 845 | document.body.appendChild(div) 846 | assert.strictEqual(div.style.marginLeft, '10px') 847 | assert.strictEqual(div.style.paddingLeft, '25px') 848 | assert.strictEqual(div.style.display, 'inline-block') 849 | v.put('35px') 850 | return new Promise(requestAnimationFrame).then(function() { 851 | assert.strictEqual(div.style.paddingLeft, '35px') 852 | }) 853 | }) 854 | 855 | test('attributeProperties', function() { 856 | var b = new Variable(true) 857 | var button = new Button({ 858 | disabled: b, 859 | name: 'MyButton' 860 | }) 861 | document.body.appendChild(button) 862 | assert.strictEqual(button.getAttribute('disabled'), '') 863 | assert.strictEqual(button.name, 'MyButton') 864 | 865 | var v = new Variable('one') 866 | var label = new Label({ 867 | role: 'button', 868 | for: v 869 | }) 870 | document.body.appendChild(label) 871 | assert.strictEqual(label.getAttribute('role'), 'button') 872 | assert.strictEqual(label.htmlFor, 'one') 873 | v.put('two') 874 | b.put(false) 875 | return new Promise(requestAnimationFrame).then(function() { 876 | assert.isFalse(button.hasAttribute('disabled')) 877 | assert.strictEqual(label.htmlFor, 'two') 878 | }) 879 | }) 880 | 881 | test('overrideDefaultWithRender', function() { 882 | var widthRendered, textContentRendered 883 | var MyComponent = extend(Div, { 884 | _renderWidth: function() { 885 | widthRendered = true 886 | }, 887 | _renderTextContent: function() { 888 | textContentRendered = true 889 | } 890 | }) 891 | var component = new MyComponent({ 892 | width: '10px' 893 | }) 894 | assert.strictEqual(component.style.width, '') // shouldn't get set 895 | assert.strictEqual(widthRendered, true) 896 | widthRendered = false 897 | component.width = '20px' 898 | assert.strictEqual(widthRendered, true) 899 | assert.strictEqual(component.width, '20px') 900 | component.textContent = 'hello' 901 | assert.strictEqual(textContentRendered, true) 902 | assert.strictEqual(component.textContent, 'hello') 903 | assert.strictEqual(component.innerHTML, '') // make sure it blocks normal textContent 904 | }) 905 | test('lifecycle', function() { 906 | var created = false 907 | var attached = 0 908 | var detached = 0 909 | var MyDiv = extend(Div, { 910 | created: function(properties) { 911 | if (properties.foo) { 912 | created = true 913 | } 914 | var div = document.createElement('div') 915 | this.appendChild(div) 916 | }, 917 | attached: function() { 918 | attached++ 919 | }, 920 | detached: function() { 921 | detached++ 922 | } 923 | }) 924 | var parentEl = document.createElement('div') 925 | document.body.appendChild(parentEl) 926 | var div = new MyDiv({foo: 'bar'}) 927 | assert.isTrue(created) 928 | parentEl.appendChild(div) 929 | return new Promise(requestAnimationFrame).then(function() { 930 | assert.equal(attached, 1, 'expected a single attach event') 931 | parentEl.removeChild(div) 932 | document.body.removeChild(parentEl) 933 | return new Promise(requestAnimationFrame).then(function() { 934 | assert.equal(detached, 1, 'expected a single detach event') 935 | }) 936 | }) 937 | }) 938 | 939 | test('assignElement', function() { 940 | var v1 = new Variable() 941 | var div = document.body.appendChild(new Div({ 942 | title: v1, 943 | })) 944 | var v2 = new Variable() 945 | assign(div, { 946 | title: v2 947 | }) 948 | v2.put('foo') 949 | return new Promise(requestAnimationFrame).then(function() { 950 | assert.equal(div.title, 'foo') 951 | v1.put('bar') // should be overriden, should have no effect 952 | return new Promise(requestAnimationFrame).then(function() { 953 | assert.equal(div.title, 'foo') 954 | }) 955 | }) 956 | }) 957 | 958 | test('assignConstructor', function() { 959 | var MyDiv = Div() 960 | assign(MyDiv, { 961 | title: 'foo' 962 | }) 963 | var div = document.body.appendChild(new MyDiv()) 964 | assert.equal(div.title, 'foo') 965 | }) 966 | 967 | test('content0', function() { 968 | var div = document.body.appendChild(new Div([Span(0), new Span(0), Span({content: 0})])) 969 | assert.equal(div.childNodes[0].textContent, '0') 970 | assert.equal(div.childNodes[1].textContent, '0') 971 | assert.equal(div.childNodes[2].textContent, '0') 972 | }) 973 | 974 | test('undefinedNode', function() { 975 | var div = document.body.appendChild(new Div([undefined, null, UL])) 976 | assert.equal(div.childNodes[0].tagName, 'UL') 977 | assert.equal(div.childNodes[1], undefined) 978 | }) 979 | 980 | test('listUpdateDuringPromise', function() { 981 | var v = new VArray(new Promise(function (resolve) { 982 | setTimeout(function() { 983 | resolve([1, 2]) 984 | }) 985 | })) 986 | var div = document.body.appendChild(new Div( 987 | v.map(function(v) { console.log("rerunning trasnform", v);return v * 2 }) 988 | )) 989 | var lastPromise 990 | v.put(lastPromise = new Promise(function (resolve) { 991 | setTimeout(function() { 992 | console.log('resolving 3,4') 993 | resolve([3, 4]) 994 | }) 995 | })) 996 | return lastPromise.then(function() { 997 | return new Promise(setTimeout).then(function() { 998 | return new Promise(function(resolve) { 999 | setTimeout(resolve, 250) 1000 | }).then(function() { 1001 | assert.equal(div.innerHTML, '68') 1002 | }) 1003 | }) 1004 | }) 1005 | }) 1006 | test('promiseInOperator', function() { 1007 | var source = new Variable() 1008 | var promised = source.to(function(s) { 1009 | return s && new Promise(function(r) { 1010 | setTimeout(function() { console.log(new Error('RESOLVING PROMISE')); r('promised'); }, 500) 1011 | }) 1012 | }) 1013 | //source._debug = 'source' 1014 | //promised._debug = 'promised' 1015 | var notLoading = 1016 | operators.or(operators.not(source), promised).whileResolving(false) 1017 | // Variable.all([source, promised]).to(function(arr) { 1018 | // var s = arr[0] 1019 | // var p = arr[1] 1020 | // var v = !s || p 1021 | // }) 1022 | .to(function(v) { 1023 | console.log('notLoading value:', v) 1024 | console.log('returning', !!v) 1025 | return !!v 1026 | }) 1027 | 1028 | var loadingSpinner = new Div({ classes: { 1029 | hidden: notLoading 1030 | }}) 1031 | document.body.appendChild(loadingSpinner) 1032 | 1033 | return new Promise(requestAnimationFrame).then(function() { 1034 | assert.strictEqual(loadingSpinner.className, 'hidden', 'expected to be hidden as source has no value') 1035 | //var upstream = new Variable() 1036 | //upstream._debug = 'upstream' 1037 | //source.put(upstream) 1038 | return new Promise(requestAnimationFrame).then(function() { 1039 | assert.strictEqual(loadingSpinner.className, 'hidden', 'expected to be hidden as source still has no value') 1040 | source.put('upstream') 1041 | return new Promise(requestAnimationFrame).then(function() { 1042 | assert.notEqual(loadingSpinner.className, 'hidden', 'expected to be SHOWN as source has value, but promise has not resolved') 1043 | return new Promise(function(r) { setTimeout(function() { requestAnimationFrame(function() { r() }) }, 500) }).then(function() { 1044 | assert.strictEqual(loadingSpinner.className, 'hidden', 'expected to be hidden promised has resolved') 1045 | }) 1046 | }) 1047 | }) 1048 | }) 1049 | }) 1050 | test('reattach', function() { 1051 | var content = new Variable('test') 1052 | var d = new Div(content) 1053 | document.body.appendChild(d) 1054 | return new Promise(requestAnimationFrame).then(function() { 1055 | document.body.removeChild(d) 1056 | return new Promise(requestAnimationFrame).then(function() { 1057 | content.put('changed') 1058 | return new Promise(requestAnimationFrame).then(function() { 1059 | var d2 = document.body.appendChild(new Div()) 1060 | d2.appendChild(d) 1061 | return new Promise(requestAnimationFrame).then(function() { 1062 | assert.equal(d.innerHTML, 'changed') 1063 | }) 1064 | }) 1065 | }) 1066 | }) 1067 | }) 1068 | 1069 | test('reattach list', function() { 1070 | var content = new VArray(['a', 'b']) 1071 | var d = new Div(content.map(function(value) { 1072 | return Span(value) 1073 | })) 1074 | document.body.appendChild(d) 1075 | return new Promise(requestAnimationFrame).then(function() { 1076 | document.body.removeChild(d) 1077 | return new Promise(requestAnimationFrame).then(function() { 1078 | content.push('c') 1079 | return new Promise(requestAnimationFrame).then(function() { 1080 | var d2 = document.body.appendChild(new Div()) 1081 | d2.appendChild(d) 1082 | return new Promise(requestAnimationFrame).then(function() { 1083 | assert.equal(d.textContent, 'abc') 1084 | }) 1085 | }) 1086 | }) 1087 | }) 1088 | }) 1089 | test('reattach bound element class', function() { 1090 | var content = new Variable('test') 1091 | var MyDiv = Element.bindElementClass(Div('.reattaching', { 1092 | textContent: content 1093 | })) 1094 | var d = new MyDiv(content) 1095 | document.body.appendChild(d) 1096 | return new Promise(requestAnimationFrame).then(function() { 1097 | document.body.removeChild(d) 1098 | return new Promise(requestAnimationFrame).then(function() { 1099 | content.put('changed') 1100 | return new Promise(requestAnimationFrame).then(function() { 1101 | var d2 = document.body.appendChild(new Div()) 1102 | d2.appendChild(d) 1103 | return new Promise(requestAnimationFrame).then(function() { 1104 | assert.equal(d.innerHTML, 'changed') 1105 | }) 1106 | }) 1107 | }) 1108 | }) 1109 | }) 1110 | test('performanceBaseline', function() { 1111 | var container = document.body.appendChild(document.createElement('div')) 1112 | for (var i = 0; i < 100; i++) { 1113 | var childDiv = container.appendChild(document.createElement('div')) 1114 | childDiv.appendChild(document.createElement('span')) 1115 | childDiv.appendChild(document.createElement('input')).className = 'test' 1116 | container.innerHTML = '' 1117 | } 1118 | 1119 | }) 1120 | test('performance', function() { 1121 | var container = document.body.appendChild(document.createElement('div')) 1122 | for (var i = 0; i < 100; i++) { 1123 | var a = new Variable('a') 1124 | container.appendChild(new Div([ 1125 | Span(a), 1126 | Input('.test') 1127 | ])) 1128 | container.innerHTML = '' 1129 | } 1130 | }) 1131 | }) 1132 | }); 1133 | -------------------------------------------------------------------------------- /tests/Renderer.js: -------------------------------------------------------------------------------- 1 | define([ 2 | '../Renderer', 3 | '../Variable', 4 | '../util/lang', 5 | 'bluebird/js/browser/bluebird' 6 | ], function (Renderer, VariableExports, lang, Promise) { 7 | var Variable = VariableExports.Variable 8 | var div = document.createElement('div'); 9 | var requestAnimationFrame = lang.requestAnimationFrame 10 | suite('Renderer', function() { 11 | test('PropertyRenderer', function() { 12 | document.body.appendChild(div); 13 | var variable = new Variable('start'); 14 | var renderer = new Renderer.PropertyRenderer({ 15 | variable: variable, 16 | element: div, 17 | name: 'title' 18 | }); 19 | assert.equal(div.title, 'start'); 20 | variable.put('second'); 21 | return new Promise(requestAnimationFrame).then(function(){ 22 | assert.equal(div.title, 'second'); 23 | document.body.removeChild(div); 24 | // now the change should not affect it 25 | variable.put('third'); 26 | return new Promise(requestAnimationFrame).then(function(){ 27 | assert.equal(div.title, 'second'); 28 | document.body.appendChild(div); 29 | Renderer.onShowElement(div); // now it should be shown and updated 30 | return new Promise(requestAnimationFrame).then(function(){ 31 | assert.equal(div.title, 'third'); 32 | }); 33 | }); 34 | }); 35 | }) 36 | test('ContentRenderer', function() { 37 | document.body.appendChild(div); 38 | var variable = new Variable('start'); 39 | var renderer = new Renderer.ContentRenderer({ 40 | variable: variable, 41 | element: div 42 | }); 43 | assert.equal(div.innerHTML, 'start'); 44 | variable.put('next'); 45 | return new Promise(requestAnimationFrame).then(function(){ 46 | assert.equal(div.innerHTML, 'next'); 47 | }); 48 | }) 49 | test('TextRenderer', function() { 50 | var div = document.createElement('div') 51 | var textNode = div.appendChild(document.createTextNode('')) 52 | document.body.appendChild(div); 53 | var variable = new Variable('start'); 54 | var renderer = new Renderer.TextRenderer({ 55 | variable: variable, 56 | element: div, 57 | textNode: textNode 58 | }); 59 | assert.equal(div.innerHTML, 'start'); 60 | variable.put('next'); 61 | return new Promise(requestAnimationFrame).then(function(){ 62 | assert.equal(div.innerHTML, 'next'); 63 | }); 64 | }) 65 | test('PromisedUpdate', function() { 66 | var resolvePromise; 67 | var promise = new Promise(function(resolve){ 68 | resolvePromise = resolve; 69 | }); 70 | var variable = new Variable(promise); 71 | var renderer = new Renderer.PropertyRenderer({ 72 | variable: variable, 73 | element: div, 74 | renderUpdate: function(value){ 75 | assert.equal(value, 4); 76 | } 77 | }); 78 | resolvePromise(4); 79 | 80 | return variable.then(function() { 81 | return new Promise(requestAnimationFrame); 82 | }); 83 | 84 | }) 85 | test('getPropertyWithVariable', function() { 86 | var foo = new Variable('foo') 87 | var bar = new Variable({ foo: foo }) 88 | var renderer = new Renderer.PropertyRenderer({ 89 | variable: bar.property('foo'), 90 | element: div, 91 | name: 'foo' 92 | }); 93 | return new Promise(requestAnimationFrame).then(function(){ 94 | assert.equal(div.foo, 'foo') 95 | }) 96 | }) 97 | test('transform with Renderer', function() { 98 | var state = new Variable({ 99 | selection: [] 100 | }) 101 | var transformed = 0 102 | var data = new Variable(3).to(function(v) { 103 | transformed++ 104 | return v + 1 105 | }) 106 | var result = Variable.all([data, state.property('selection') ]).to(function(args) { 107 | return args[0] + args[1].length 108 | }) 109 | var div = document.createElement('div') 110 | document.body.appendChild(div) 111 | var updated 112 | new Renderer.ElementRenderer({ 113 | variable: result, 114 | element: div, 115 | renderUpdate: function() { 116 | updated = true 117 | } 118 | }) 119 | return new Promise(requestAnimationFrame).then(function(){ 120 | assert.equal(transformed, 1) 121 | state.set('selection', ['a']) 122 | return new Promise(requestAnimationFrame).then(function(){ 123 | assert.equal(transformed, 1) 124 | }) 125 | }) 126 | }) 127 | test('Renderer with complex transform', function() { 128 | 129 | var data = new Variable([{ 130 | id: 1, 131 | group: 1, 132 | text: 'foo' 133 | }, { 134 | id: 2, 135 | group: 1, 136 | text: 'bar' 137 | }, { 138 | id: 3, 139 | group: 2, 140 | text: 'baz' 141 | }]) 142 | 143 | var count = 0 144 | 145 | var transformedData = data.to(function(data) { 146 | count++ 147 | if (count > 1) { 148 | throw new Error('Not cached properly') 149 | } 150 | return { 151 | data: data.map(function(datum){ 152 | return lang.copy({}, datum) 153 | }), 154 | otherStuff: 'lolwut', 155 | } 156 | }) 157 | 158 | var state = new Variable({ 159 | selection: {} 160 | }) 161 | 162 | var selectedGroups = Variable.all([ 163 | state.property('selection'), 164 | transformedData.property('data') 165 | ], function(selection, data) { 166 | return data.reduce(function(memo, datum) { 167 | if (selection[datum.id]) { 168 | memo[datum.group] = true 169 | } 170 | 171 | return memo 172 | }, {}) 173 | }) 174 | 175 | var div = document.createElement('div') 176 | document.body.appendChild(div) 177 | var updated 178 | 179 | new Renderer.ElementRenderer({ 180 | variable: selectedGroups, 181 | element: div, 182 | renderUpdate: function(selectedGroups) { 183 | console.log('selectedGroups', selectedGroups) 184 | console.log('this.count === 1?', count === 1) 185 | } 186 | }) 187 | 188 | setTimeout(function() { state.set('selection', { 1: true }) }, 10) 189 | setTimeout(function() { state.set('selection', { 1: true, 2: true }) }, 50) 190 | return new Promise(function(resolve, reject) { 191 | setTimeout(function() { 192 | state.set('selection', { 3: true }) 193 | resolve() 194 | }, 100) 195 | }) 196 | }) 197 | 198 | test('RendererInvalidation', function() { 199 | document.body.appendChild(div); 200 | var outer = new Variable(false); 201 | var signal = new Variable(); 202 | var arr = [1,2,3]; 203 | var data = signal.to(function() { return arr; }); 204 | var inner = data.to(function(a) { return a.map(function(o) { return o*2; }); }); 205 | var derived = outer.to(function (o) { 206 | return inner.to(function(i){ 207 | return [o, i]; 208 | }); 209 | }); 210 | var valuesSeen = []; 211 | var renderer = new Renderer.ElementRenderer({ 212 | variable: derived, 213 | element: div, 214 | renderUpdate: function (value) { 215 | valuesSeen.push(value); 216 | } 217 | }); 218 | 219 | var INTERMEDIATE_ANIMATION_FRAME = false; 220 | var resolver = function(r){r();}; // resolve asynchronously without an animation frame 221 | // we expect the renderer to observe the inner array mutation regardless of the outer end state 222 | var firstResult = [false, [2,4,6]]; 223 | var lastResult = [false, [8,10,12]]; 224 | var expected = [firstResult,lastResult]; 225 | 226 | if (INTERMEDIATE_ANIMATION_FRAME) { 227 | // request an intermediate animation frame 228 | resolver = requestAnimationFrame; 229 | // the intermediate frame observes the outer state change 230 | expected = [firstResult, [true,[2,4,6]], lastResult] 231 | } 232 | 233 | return new Promise(requestAnimationFrame).then(function(){ 234 | // confirm the initial state 235 | assert.deepEqual(valuesSeen, [firstResult]); 236 | // processing 237 | outer.put(true); 238 | // simulate an async task 239 | return new Promise(resolver).then(function() { 240 | // do some "work" 241 | arr = [4,5,6]; 242 | // invalidate the data signal 243 | signal.updated(); 244 | }).then(function() { 245 | // done processing 246 | outer.put(false); 247 | 248 | // UI should now reflect changes to data array 249 | 250 | return new Promise(requestAnimationFrame).then(function() { 251 | assert.deepEqual(valuesSeen, expected); 252 | }); 253 | }); 254 | }); 255 | }) 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /tests/all.js: -------------------------------------------------------------------------------- 1 | define([ 2 | './unit', 3 | './Renderer', 4 | './Element', 5 | './dstore', 6 | './es6' 7 | ], function () { 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dgrid.js: -------------------------------------------------------------------------------- 1 | define([ 2 | '../Variable', 3 | '../extensions/dstore', 4 | 'dstore/Memory', 5 | 'intern!object', 6 | 'intern/chai!assert' 7 | ], function (VariableExports, dstore, Memory, registerSuite, assert) { 8 | var Variable = VariableExports.Variable 9 | var DstoreVariable = dstore.DstoreVariable 10 | function MockStore() { 11 | this.handlers = [] 12 | this.on = function(events, f) { 13 | this.handlers.push(f) 14 | var that = this 15 | return { 16 | remove: function() { 17 | that.handlers = that.handlers.filter(function(h) { return h !== f }) 18 | } 19 | } 20 | } 21 | } 22 | suite('dstore', function() { 23 | test('dstoreUpdates', function() { 24 | var store = new Memory({ 25 | data: [ 26 | {id: 1, name: 'one'}, 27 | {id: 2, name: 'two'}, 28 | {id: 3, name: 'three'} 29 | ] 30 | }) 31 | // wrap an existing variable just to make sure we get flow through 32 | var array = new DstoreVariable(new Variable(store)) 33 | var count = array.to(function(){ 34 | return store.fetchSync().length 35 | }) 36 | var lastCountUpdate 37 | count.notifies({ 38 | updated: function(updateEvent) { 39 | lastCountUpdate = updateEvent 40 | } 41 | }) 42 | 43 | assert.strictEqual(count.valueOf(), 3) 44 | store.add({id: 4, name: 'four'}) 45 | assert.strictEqual(count.valueOf(), 4) 46 | assert.strictEqual(lastCountUpdate.type, 'refresh') 47 | lastCountUpdate = null 48 | store.remove(2) 49 | assert.strictEqual(count.valueOf(), 3) 50 | assert.strictEqual(lastCountUpdate.type, 'refresh') 51 | lastCountUpdate = null 52 | store.put({id: 4, name: 'FOUR'}) 53 | assert.strictEqual(count.valueOf(), 3) 54 | assert.strictEqual(lastCountUpdate.type, 'refresh') 55 | }) 56 | test('resolveDStoreAsyncPromise', function() { 57 | var store = new Memory({ 58 | data: [ 59 | {id: 1, name: 'one'}, 60 | {id: 2, name: 'two'}, 61 | {id: 3, name: 'three'} 62 | ] 63 | }) 64 | //var storeVar = new DstoreVariable(store) 65 | var storeVar = new Variable(store) 66 | var result = [] 67 | storeVar.forEach(function(item){ 68 | result.push({i: item.id, n: item.name}) 69 | }) 70 | assert.deepEqual(result, [{i:1,n:'one'},{i:2,n:'two'},{i:3,n:'three'}]) 71 | }) 72 | test('listenerRemovedWhenVariableChanged', function() { 73 | var m1 = new MockStore() 74 | var m2 = new MockStore() 75 | assert.equal(m1.handlers.length, 0) 76 | assert.equal(m2.handlers.length, 0) 77 | var storeVar = new DstoreVariable() 78 | storeVar.put(m1) 79 | storeVar.valueOf() 80 | assert.equal(m1.handlers.length, 1) 81 | assert.equal(m2.handlers.length, 0) 82 | storeVar.put(m2) 83 | storeVar.valueOf() 84 | assert.equal(m1.handlers.length, 0) 85 | assert.equal(m2.handlers.length, 1) 86 | }) 87 | test('listenerRegistrationIdempotent', function() { 88 | var m1 = new MockStore() 89 | var m2 = new MockStore() 90 | assert.equal(m1.handlers.length, 0) 91 | assert.equal(m2.handlers.length, 0) 92 | var storeVar = new DstoreVariable() 93 | storeVar.put(m1) 94 | storeVar.valueOf() 95 | assert.equal(m1.handlers.length, 1) 96 | storeVar.valueOf() 97 | assert.equal(m1.handlers.length, 1) 98 | storeVar.put(m1) 99 | storeVar.valueOf() 100 | assert.equal(m1.handlers.length, 1) 101 | storeVar.put(m2) 102 | storeVar.valueOf() 103 | assert.equal(m1.handlers.length, 0) 104 | assert.equal(m2.handlers.length, 1) 105 | // put seen m1 again 106 | storeVar.put(m1) 107 | storeVar.valueOf() 108 | assert.equal(m1.handlers.length, 1) 109 | assert.equal(m2.handlers.length, 0) 110 | } 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /tests/dstore.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | var VariableExports = require('../Variable') 3 | var dstore = require('../extensions/dstore') 4 | var Memory = require('dojo-dstore/Memory') 5 | var Variable = VariableExports.Variable 6 | var DstoreVariable = dstore.DstoreVariable 7 | function MockStore() { 8 | this.handlers = [] 9 | this.on = function(events, f) { 10 | this.handlers.push(f) 11 | var that = this 12 | return { 13 | remove: function() { 14 | that.handlers = that.handlers.filter(function(h) { return h !== f }) 15 | } 16 | } 17 | } 18 | } 19 | suite('dstore', function() { 20 | test('dstoreUpdates', function() { 21 | var store = new Memory({ 22 | data: [ 23 | {id: 1, name: 'one'}, 24 | {id: 2, name: 'two'}, 25 | {id: 3, name: 'three'} 26 | ] 27 | }) 28 | // wrap an existing variable just to make sure we get flow through 29 | var array = new DstoreVariable(new Variable(store)) 30 | var count = array.to(function(){ 31 | return store.fetchSync().length 32 | }) 33 | var lastCountUpdate 34 | count.notifies({ 35 | updated: function(updateEvent) { 36 | lastCountUpdate = updateEvent 37 | } 38 | }) 39 | 40 | assert.strictEqual(count.valueOf(), 3) 41 | store.add({id: 4, name: 'four'}) 42 | assert.strictEqual(count.valueOf(), 4) 43 | assert.strictEqual(lastCountUpdate.type, 'replaced') 44 | lastCountUpdate = null 45 | store.remove(2) 46 | assert.strictEqual(count.valueOf(), 3) 47 | assert.strictEqual(lastCountUpdate.type, 'replaced') 48 | lastCountUpdate = null 49 | store.put({id: 4, name: 'FOUR'}) 50 | assert.strictEqual(count.valueOf(), 3) 51 | assert.strictEqual(lastCountUpdate.type, 'replaced') 52 | }) 53 | test('resolveDStoreAsyncPromise', function() { 54 | var store = new Memory({ 55 | data: [ 56 | {id: 1, name: 'one'}, 57 | {id: 2, name: 'two'}, 58 | {id: 3, name: 'three'} 59 | ] 60 | }) 61 | var storeVar = new DstoreVariable(store) 62 | //var storeVar = new Variable(store) 63 | var result = [] 64 | storeVar.forEach(function(item){ 65 | result.push({i: item.id, n: item.name}) 66 | }) 67 | assert.deepEqual(result, [{i:1,n:'one'},{i:2,n:'two'},{i:3,n:'three'}]) 68 | }) 69 | test('listenerRemovedWhenVariableChanged', function() { 70 | var m1 = new MockStore() 71 | var m2 = new MockStore() 72 | assert.equal(m1.handlers.length, 0) 73 | assert.equal(m2.handlers.length, 0) 74 | var storeVar = new DstoreVariable() 75 | storeVar.put(m1) 76 | storeVar.valueOf() 77 | assert.equal(m1.handlers.length, 1) 78 | assert.equal(m2.handlers.length, 0) 79 | storeVar.put(m2) 80 | storeVar.valueOf() 81 | assert.equal(m1.handlers.length, 0) 82 | assert.equal(m2.handlers.length, 1) 83 | }) 84 | test('listenerRegistrationIdempotent', function() { 85 | var m1 = new MockStore() 86 | var m2 = new MockStore() 87 | assert.equal(m1.handlers.length, 0) 88 | assert.equal(m2.handlers.length, 0) 89 | var storeVar = new DstoreVariable() 90 | storeVar.put(m1) 91 | storeVar.valueOf() 92 | assert.equal(m1.handlers.length, 1) 93 | storeVar.valueOf() 94 | assert.equal(m1.handlers.length, 1) 95 | storeVar.put(m1) 96 | storeVar.valueOf() 97 | assert.equal(m1.handlers.length, 1) 98 | storeVar.put(m2) 99 | storeVar.valueOf() 100 | assert.equal(m1.handlers.length, 0) 101 | assert.equal(m2.handlers.length, 1) 102 | // put seen m1 again 103 | storeVar.put(m1) 104 | storeVar.valueOf() 105 | assert.equal(m1.handlers.length, 1) 106 | assert.equal(m2.handlers.length, 0) 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /tests/es6.js: -------------------------------------------------------------------------------- 1 | define([ 2 | '../Element', 3 | '../Variable', 4 | ], function (Element, VariableExports) { 5 | function valueOfAndNotify(variable, callback) { 6 | var value = variable.valueOf() 7 | variable.notifies(typeof callback === 'object' ? callback : { 8 | updated: callback 9 | }) 10 | return value 11 | } 12 | var Variable = VariableExports.Variable 13 | var VSet = VariableExports.VSet 14 | var react = VariableExports.react 15 | var Div = Element.Div 16 | var Button = Element.Button 17 | var defineElement = Element.defineElement 18 | var element = Element.element 19 | suite('es6', function() { 20 | test('react', function() { 21 | var a = new Variable() 22 | var b = new Variable() 23 | var sum = react(function*() { 24 | return (yield a) + (yield b) 25 | }) 26 | var invalidated = false 27 | valueOfAndNotify(sum, function() { 28 | invalidated = true 29 | }) 30 | var target = new Variable() 31 | target.put(sum) 32 | var targetInvalidated = false 33 | valueOfAndNotify(target, function() { 34 | targetInvalidated = true 35 | }) 36 | a.put(3) 37 | // assert.isFalse(invalidated) 38 | b.put(5) 39 | //assert.isTrue(invalidated) 40 | assert.equal(sum.valueOf(), 8) 41 | invalidated = false 42 | assert.equal(target.valueOf(), 8) 43 | targetInvalidated = false 44 | a.put(4) 45 | assert.isTrue(invalidated) 46 | assert.equal(sum.valueOf(), 9) 47 | invalidated = false 48 | assert.isTrue(targetInvalidated) 49 | assert.equal(target.valueOf(), 9) 50 | targetInvalidated = false 51 | b.put(6) 52 | assert.isTrue(invalidated) 53 | assert.equal(sum.valueOf(), 10) 54 | assert.isTrue(targetInvalidated) 55 | assert.equal(target.valueOf(), 10) 56 | 57 | }) 58 | 59 | test('elementClass', function() { 60 | class MyDiv extends Div('.my-class', {title: 'my-title', wasClicked: false}) { 61 | onclick() { 62 | this.otherMethod() 63 | } 64 | otherMethod() { 65 | this.wasClicked = true 66 | } 67 | } 68 | var myDiv = new MyDiv() 69 | assert.isFalse(myDiv.wasClicked) 70 | myDiv.click() 71 | assert.isTrue(myDiv.wasClicked) 72 | class MySubDiv extends MyDiv { 73 | otherMethod() { 74 | super.otherMethod() 75 | this.alsoFlagged = true 76 | } 77 | } 78 | var mySubDiv = new MySubDiv() 79 | assert.isFalse(mySubDiv.wasClicked) 80 | mySubDiv.click() 81 | assert.isTrue(mySubDiv.wasClicked) 82 | assert.isTrue(mySubDiv.alsoFlagged) 83 | }) 84 | test('forOf', function() { 85 | var arrayVariable = new Variable(['a', 'b', 'c']) 86 | var results = [] 87 | for (let letter of arrayVariable) { 88 | results.push(letter) 89 | } 90 | assert.deepEqual(results, ['a', 'b', 'c']) 91 | }) 92 | test('Symbol', function() { 93 | let mySymbol = Symbol('my') 94 | let obj = { 95 | [mySymbol]: 'test' 96 | } 97 | let v = new Variable(obj) 98 | assert.strictEqual(v.property(mySymbol).valueOf(), 'test') 99 | }) 100 | test('Map', function() { 101 | let map = new Map() 102 | map.set('a', 2) 103 | var v = new Variable.VMap(map) 104 | var updated 105 | v.property('a').notifies({ 106 | updated: function(){ 107 | updated = true 108 | } 109 | }) 110 | assert.strictEqual(v.get('a'), 2) 111 | updated = false 112 | v.set('a', 3) 113 | assert.strictEqual(updated, true) 114 | assert.strictEqual(v.get('a'), 3) 115 | assert.strictEqual(map.get('a'), 3) 116 | v.set(2, 2) 117 | v.set('2', 'two') 118 | assert.strictEqual(map.get(2), 2) 119 | assert.strictEqual(map.get('2'), 'two') 120 | }) 121 | test('renderGenerator', function() { 122 | var a = new Variable(1) 123 | var b = new Variable(2) 124 | class GeneratorDiv extends Div { 125 | *render(properties) { 126 | this.textContent = (yield a) + (yield b) 127 | } 128 | } 129 | function *render2() { 130 | this.textContent = 5 + (yield b) 131 | } 132 | var div = new GeneratorDiv({ 133 | foo: 3 134 | }) 135 | var Div2 = Div({ 136 | render: render2 137 | }) 138 | var div2 139 | document.body.appendChild(div) 140 | document.body.appendChild(div2 = new Div2()) 141 | return new Promise(requestAnimationFrame).then(function(){ 142 | assert.strictEqual(div.textContent, '3') 143 | assert.strictEqual(div2.textContent, '7') 144 | a.put(2) 145 | return new Promise(requestAnimationFrame).then(function(){ 146 | assert.strictEqual(div.textContent, '4') 147 | assert.strictEqual(div2.textContent, '7') 148 | b.put(3) 149 | return new Promise(requestAnimationFrame).then(function(){ 150 | assert.strictEqual(div.textContent, '5') 151 | assert.strictEqual(div2.textContent, '8') 152 | }) 153 | }) 154 | }) 155 | }) 156 | test('reactPromises', function() { 157 | let currentResolver, currentRejecter, sum = 0, done = false 158 | let errorThrown 159 | var sequence = react(function*() { 160 | while (!done) { 161 | try { 162 | sum += yield new Promise((resolve, reject) => { 163 | currentResolver = resolve 164 | currentRejecter = reject 165 | }) 166 | } catch (e) { 167 | errorThrown = e 168 | } 169 | } 170 | return sum 171 | }) 172 | var sequencePromise = sequence.then(function(value) { return value }) // start it 173 | currentResolver(2) 174 | return Promise.resolve().then(() => { 175 | assert.strictEqual(sum, 2) 176 | currentResolver(3) 177 | return Promise.resolve().then(() => { 178 | assert.strictEqual(sum, 5) 179 | currentResolver(4) 180 | return Promise.resolve().then(() => { 181 | assert.strictEqual(sum, 9) 182 | currentRejecter('error occurred') 183 | return Promise.resolve().then(() => { 184 | assert.strictEqual(errorThrown, 'error occurred') 185 | done = true 186 | currentResolver(1) 187 | return Promise.resolve().then(() => { 188 | assert.strictEqual(sum, 10) 189 | return sequencePromise.then((result) => { 190 | assert.strictEqual(result, 10) 191 | }) 192 | }) 193 | }) 194 | }) 195 | }) 196 | }) 197 | }) 198 | test('getPropertyWithClass', function() { 199 | class Foo extends Variable { 200 | constructor() { 201 | super(...arguments) 202 | // prelude to class properties 203 | this.baz = this.property('baz') 204 | this.baz2 = this.property('baz') 205 | this.structured = true // make sure this doesn't change anything 206 | } 207 | } 208 | class Bar extends Variable { 209 | constructor() { 210 | super(...arguments) 211 | this.foo = this.property('foo', Foo) 212 | this.isWritable = false 213 | } 214 | } 215 | var obj = { foo: { baz: 3 } } 216 | var bar = new Bar(obj) 217 | assert.strictEqual(bar.foo.baz.valueOf(), 3) 218 | assert.strictEqual(bar.foo.baz2.valueOf(), 3) 219 | bar.foo.baz.put(5) 220 | assert.strictEqual(obj.foo.baz, 5) 221 | }) 222 | test('structuredClass', function() { 223 | class Foo extends Variable { 224 | constructor() { 225 | super(...arguments) 226 | // prelude to class properties 227 | this.baz = new Variable() 228 | this.bazDerived = this.baz.to(baz => baz * 2) 229 | this.structured = true 230 | } 231 | } 232 | class Bar extends Variable { 233 | constructor() { 234 | super(...arguments) 235 | this.foo = new Foo() 236 | this.structured = true 237 | this.isWritable = false 238 | } 239 | } 240 | var obj = { foo: { baz: 3 } } 241 | var bar = new Bar(obj) 242 | assert.strictEqual(bar.foo.baz.valueOf(), 3) 243 | assert.strictEqual(bar.foo.bazDerived.valueOf(), 6) 244 | bar.foo.baz.put(5) 245 | assert.strictEqual(obj.foo.baz, 5) 246 | assert.strictEqual(bar.property('foo').get('baz'), 5) 247 | assert.strictEqual(bar.property('foo').get('bazDerived'), 10) 248 | }) 249 | test('proxiedVariable', function() { 250 | let Foo = Variable({ 251 | a: Variable, 252 | *derived() { 253 | return 4 + (yield this.a) 254 | } 255 | }) 256 | let foo = Foo.proxy({a: 3, bar: 'hi'}) 257 | let aInvalidated, barInvalidated, derivedInvalidated 258 | assert.strictEqual(valueOfAndNotify(foo.a, function() { 259 | aInvalidated = true 260 | }), 3) 261 | assert.strictEqual(valueOfAndNotify(foo.bar, function() { 262 | barInvalidated = true 263 | }), 'hi') 264 | assert.strictEqual(valueOfAndNotify(foo.derived, function() { 265 | derivedInvalidated = true 266 | }), 7) 267 | foo.a = 5 268 | assert.isTrue(aInvalidated) 269 | assert.isTrue(derivedInvalidated) 270 | assert.isTrue(foo.derived == 9) 271 | foo.bar = 'hello' 272 | assert.isTrue(barInvalidated) 273 | assert.isTrue(foo.bar == 'hello') 274 | delete foo.bar 275 | assert.isTrue(foo.bar.valueOf() == undefined) 276 | }) 277 | test('instanceofElementClass', function() { 278 | var MyDiv = Div('.test') 279 | // only do this if the language supports Symbol.hasInstance 280 | //assert.isTrue(MyDiv instanceof Element.ElementClass) 281 | }) 282 | test('registerTag', function() { 283 | class CustomElementTemp extends Element { 284 | constructor() { 285 | super(...arguments) 286 | this.bar = 4 287 | } 288 | foo() { 289 | return 3 290 | } 291 | } 292 | var CustomElement = defineElement('custom-tag', CustomElementTemp) 293 | 294 | var custom = new CustomElement()//.create() 295 | assert.equal(custom.tagName.toUpperCase(), 'CUSTOM-TAG') 296 | assert.equal(custom.foo(), 3) 297 | assert.equal(custom.bar, 4) 298 | var custom = new CustomElement('.some-class', { content: 'hi' }) 299 | assert.equal(custom.tagName.toUpperCase(), 'CUSTOM-TAG') 300 | assert.equal(custom.foo(), 3) 301 | assert.equal(custom.bar, 4) 302 | assert.equal(custom.className, 'some-class') 303 | assert.equal(custom.innerHTML, 'hi') 304 | }) 305 | 306 | test('getterGeneratorOnVariable', function() { 307 | var Foo = Variable({ 308 | planet: Variable, 309 | *recipient() { 310 | return yield this.planet 311 | }, 312 | *greeting() { 313 | return 'Hello, ' + (yield this.recipient) 314 | } 315 | }) 316 | var foo = new Foo({planet: 'World'}) 317 | var greeting = foo.greeting 318 | var updated 319 | assert.strictEqual(valueOfAndNotify(greeting, function() { 320 | updated = true 321 | }), 'Hello, World') 322 | foo.planet.put('Saturn') 323 | assert.isTrue(updated) 324 | assert.strictEqual(greeting.valueOf(), 'Hello, Saturn') 325 | }) 326 | test('getterGeneratorOnElement', function() { 327 | var Foo = Div({ 328 | name: Variable, 329 | *title() { 330 | return yield this.name 331 | }, 332 | *greeting() { 333 | return 'Hello, ' + (yield this.name) 334 | }, 335 | *content() { 336 | return (yield this.greeting) + '.' 337 | } 338 | }) 339 | 340 | var SubFoo = Foo({ 341 | *name() { 342 | return `${yield this.firstName} ${yield this.lastName}` 343 | } 344 | }) 345 | 346 | var name = new Variable('World') 347 | var foo = new Foo({name: name}) 348 | document.body.appendChild(foo) 349 | var lastName = new Variable('Doe') 350 | var subFoo = new SubFoo({firstName: 'John', lastName: lastName}) 351 | document.body.appendChild(subFoo) 352 | return new Promise(requestAnimationFrame).then(function(){ 353 | assert.strictEqual(foo.textContent, 'Hello, World.') 354 | assert.strictEqual(foo.title, 'World') 355 | assert.strictEqual(subFoo.textContent, 'Hello, John Doe.') 356 | assert.strictEqual(subFoo.title, 'John Doe') 357 | name.put('Moon') 358 | lastName.put('Smith') 359 | return new Promise(requestAnimationFrame).then(function(){ 360 | assert.strictEqual(foo.textContent, 'Hello, Moon.') 361 | assert.strictEqual(foo.title, 'Moon') 362 | assert.strictEqual(subFoo.textContent, 'Hello, John Smith.') 363 | assert.strictEqual(subFoo.title, 'John Smith') 364 | }) 365 | }) 366 | 367 | }) 368 | }) 369 | }) 370 | -------------------------------------------------------------------------------- /tests/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Alkali Form 6 | 7 | 8 | 9 | Litmus Link 10 | 11 | 12 | 13 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/has.js: -------------------------------------------------------------------------------- 1 | define(['dojo/has'], function(has) { 2 | has.add('promise', !!window.Promise) 3 | return has 4 | }) 5 | -------------------------------------------------------------------------------- /tests/intern.js: -------------------------------------------------------------------------------- 1 | // Learn more about configuring this file at . 2 | // These default settings work OK for most people. The options that *must* be changed below are the 3 | // packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites. 4 | 5 | define({ 6 | // The port on which the instrumenting proxy will listen 7 | proxyPort: 9000, 8 | 9 | // A fully qualified URL to the Intern proxy 10 | proxyUrl: 'http://localhost:9000/', 11 | 12 | // Default desired capabilities for all environments. Individual capabilities can be overridden by any of the 13 | // specified browser environments in the `environments` array below as well. See 14 | // https://code.google.com/p/selenium/wiki/DesiredCapabilities for standard Selenium capabilities and 15 | // https://saucelabs.com/docs/additional-config#desired-capabilities for Sauce Labs capabilities. 16 | // Note that the `build` capability will be filled in with the current commit ID from the Travis CI environment 17 | // automatically 18 | capabilities: { 19 | 'selenium-version': '2.33.0' 20 | }, 21 | 22 | // Browsers to run integration testing against. Note that version numbers must be strings if used with Sauce 23 | // OnDemand. Options that will be permutated are browserName, version, platform, and platformVersion; any other 24 | // capabilities options specified for an environment will be copied as-is 25 | environments: [ 26 | { browserName: 'internet explorer', version: '11', platform: 'Windows 8' }, 27 | // { browserName: 'firefox', version: '21', platform: 'Mac 10.6' }, 28 | { browserName: 'chrome', platform: [ 'Linux' ] } 29 | //{ browserName: 'safari', version: '6', platform: 'Mac 10.8' } 30 | ], 31 | 32 | // Maximum number of simultaneous integration tests that should be executed on the remote WebDriver service 33 | maxConcurrency: 1, 34 | 35 | // Whether or not to start Sauce Connect before running tests 36 | useSauceConnect: true, 37 | 38 | // Connection information for the remote WebDriver service. If using Sauce Labs, keep your username and password 39 | // in the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables unless you are sure you will NEVER be 40 | // publishing this configuration file somewhere 41 | webdriver: { 42 | host: 'localhost', 43 | port: 4444 44 | }, 45 | 46 | // Configuration options for the module loader; any AMD configuration options supported by the Dojo loader can be 47 | // used here 48 | loader: { 49 | baseUrl: typeof process === 'undefined' ? 50 | // if we are using the full path to alkali, we assume we are running 51 | // in a sibling path configuration 52 | location.search.indexOf('config=alkali') > -1 ? '../..' : '..' : 53 | './node_modules', 54 | 55 | // Packages that should be registered with the loader in each testing environment 56 | requestProvider: 'dojo/request/registry', 57 | packages: [ 58 | { name: 'dojo', location: 'dojo' }, 59 | { name: 'dstore', location: 'dojo-dstore' }, 60 | { 61 | name: 'alkali', 62 | location: typeof process === 'undefined' ? 63 | location.search.indexOf('config=alkali') > -1 ? 'alkali' : '..' : 64 | '..' 65 | } 66 | ] 67 | }, 68 | 69 | // Non-functional test suite(s) to run in each browser 70 | suites: typeof window === 'undefined' ? [ 'alkali/tests/unit' ] : [ 'alkali/tests/all' ], 71 | 72 | // Functional test suite(s) to run in each browser once non-functional tests are completed 73 | functionalSuites: [ 'alkali/tests/all' ], 74 | 75 | // A regular expression matching URLs to files that should not be included in code coverage analysis 76 | excludeInstrumentation: /^dojox?|^tests?\// 77 | }); 78 | -------------------------------------------------------------------------------- /tests/operators.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | var operators = require('../operators') 3 | var VariableExports = require('../Variable') 4 | var Promise = require('bluebird/js/browser/bluebird') 5 | var Variable = VariableExports.Variable 6 | var VNumber = VariableExports.VNumber 7 | var VBoolean = VariableExports.VBoolean 8 | 9 | suite('operators', function() { 10 | test('operatorType', function() { 11 | var v = new Variable(2) 12 | var v2 = new Variable(2) 13 | assert.isTrue(operators.equal(v, v2) instanceof VBoolean) 14 | assert.isTrue(operators.equal(v, v2).valueOf()) 15 | assert.isTrue(operators.add(v, v2) instanceof VNumber) 16 | assert.equal(operators.add(v, v2).valueOf(), 4) 17 | }) 18 | test('conjunctionNegation', function() { 19 | var s = new Variable() 20 | var d = s.to(function(s) { 21 | return s && new Promise(function(r) { 22 | setTimeout(function() { r('promised') }, 100) 23 | }) 24 | }) 25 | var notLoading = operators.or(operators.not(s), d) 26 | assert.isTrue(notLoading.valueOf()) 27 | var upstream = new Variable() 28 | s.put(upstream) 29 | assert.isTrue(!!notLoading.valueOf()) 30 | upstream.put('source') 31 | return new Promise(function(r) { setTimeout(function() { r() }, 50) }).then(function () { 32 | assert.isFalse(!!notLoading.valueOf(), 'expected to be still loading (source selected, derived not fulfilled)') 33 | return new Promise(function(r) { setTimeout(function() { r() }, 100) }).then(function () { 34 | assert.isTrue(!!notLoading.valueOf(), 'expected to have fully loaded (source selected and derived has been fulfilled)') 35 | }) 36 | }) 37 | }) 38 | test('round', function() { 39 | assert.equal(operators.round(new Variable(2.345)).valueOf(), 2) 40 | assert.equal(operators.round(new Variable(2.567)).valueOf(), 3) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/reactive.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | var reactive = require('../reactive') 3 | suite('reactive', function() { 4 | 5 | test('primitive types', function() { 6 | var numVar = reactive(3) 7 | assert.equal(numVar.valueOf(), 3) 8 | assert.equal(numVar.to(function(num) { return num * 2 }).valueOf(), 6) 9 | var strVar = reactive('hello') 10 | assert.equal(strVar.valueOf(), 'hello') 11 | assert.equal(strVar.toUpperCase().valueOf(), 'HELLO') 12 | var boolVar = reactive(false) 13 | assert.strictEqual(boolVar.valueOf(), false) 14 | assert.strictEqual(boolVar.to(function(bool) { return !bool }).valueOf(), true) 15 | }) 16 | test('native types', function() { 17 | if (typeof Map === 'undefined') { 18 | return 19 | } 20 | var setVar = reactive(new Set()) 21 | setVar.add(4) 22 | assert.equal(setVar.valueOf().size, 1) 23 | }) 24 | test('complex data', function() { 25 | var circular = { flag: true } 26 | circular.circular = circular 27 | var objVar = reactive({ 28 | num: 3, 29 | obj: { 30 | foo: 4, 31 | subObj: { 32 | str: 'hi' 33 | } 34 | }, 35 | circular: circular 36 | }) 37 | assert.equal(objVar.num.valueOf(), 3) 38 | assert.equal(objVar.obj.foo.valueOf(), 4) 39 | assert.equal(objVar.obj.subObj.str.valueOf(), 'hi') 40 | assert.equal(objVar.circular.circular.flag.valueOf(), true) 41 | }) 42 | test('change original data', function() { 43 | var obj = { 44 | foo: 3 45 | } 46 | reactive(obj).foo.put(3) 47 | assert.equal(obj.foo, 3) 48 | }) 49 | test('with arrays', function() { 50 | var array = reactive([{ name: 'first' }, { name: 'second' }]) 51 | var asString = array.to(function(array) { 52 | return array.map(function(item) { 53 | return item.name 54 | }).join(', ') 55 | }) 56 | assert.equal(asString.valueOf(), 'first, second') 57 | array.property(0).set('name', 'changed') 58 | assert.equal(asString.valueOf(), 'changed, second') 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 30 | 31 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["DOM", "ES2017", "ScriptHost"], 5 | "noEmit": true, 6 | "strict": true, 7 | "strictNullChecks": false 8 | }, 9 | "files": [ "./types.ts" ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Variable, VArray, VNumber, all, reactive, Reacts, VariableClass, Promise as AlkaliPromise } from 'alkali' 3 | 4 | // Variable, Primitive-Typed Variable, reactive/Reacts compatibility 5 | let vi = new Variable(1) 6 | let vn = new VNumber(1) 7 | let rn = reactive(1) 8 | let Rn: Reacts 9 | { 10 | let t = vi 11 | t.valueOf().toFixed() 12 | t = vn 13 | //t = rn 14 | t = Rn 15 | } 16 | { 17 | let t = vn 18 | t.valueOf().toFixed() 19 | //t = vi 20 | //t = rn 21 | t = Rn 22 | } 23 | { 24 | let t = rn 25 | t.valueOf().toFixed() 26 | t = vi 27 | t = vn 28 | t = Rn 29 | } 30 | { 31 | let t = Rn 32 | t.valueOf().toFixed() 33 | //t = vi 34 | t = vn 35 | //t = rn 36 | } 37 | // put, then 38 | { 39 | const p = new Variable('p') 40 | let sr: string | Variable | AlkaliPromise = p.put('string') 41 | let vr: string | Variable | AlkaliPromise = p.put(new Variable('string var')) 42 | p.then(s => s.toLowerCase()) 43 | } 44 | // subscription 45 | { 46 | const v = new Variable('a') 47 | const subscription = v.subscribe(e => { 48 | const str: string = e.value() 49 | }) 50 | subscription.unsubscribe() 51 | } 52 | 53 | // test heterogeneous variadic types 54 | { 55 | const vs = new Variable('a').to(s => `[${s}]`) 56 | const vn = new Variable(new Variable(1)) 57 | const s = 'three' 58 | const n = 4 59 | // variadic args 60 | let a: Variable<[string, number, string, number]> = all(vs, vn, s, n) 61 | let r = a.to(([str, num, str2, num2]) => { 62 | let s: string = str, n: number = num, s2: string = str2, n2: number = num2 63 | str.toLowerCase() 64 | str2.toLowerCase() 65 | num.toFixed() 66 | num2.toFixed() 67 | return { s: str, n: num } 68 | }).to(r => { 69 | r.s.toLowerCase() 70 | r.n.toFixed() 71 | return r 72 | }).valueOf() 73 | r.s.toLowerCase() 74 | r.n.toFixed() 75 | 76 | // array arg 77 | a = all([vs, vn, s, n]) 78 | r = a.to(([str, num, str2, num2]) => { 79 | let s: string = str, n: number = num, s2: string = str2, n2: number = num2 80 | str.toLowerCase() 81 | str2.toLowerCase() 82 | num.toFixed() 83 | num2.toFixed() 84 | return { s: str, n: num } 85 | }).to(r => { 86 | r.s.toLowerCase() 87 | r.n.toFixed() 88 | return r 89 | }).valueOf() 90 | r.s.toLowerCase() 91 | r.n.toFixed() 92 | } 93 | { 94 | // homogeneous primitive array type 95 | const numberArray = [1,2,3] 96 | let a: Variable = all(numberArray) 97 | a.to(([one, two, three]) => { 98 | let n: number = one, n2: number = two, n3: number = three 99 | one.toFixed() 100 | two.toFixed() 101 | three.toFixed() 102 | }) 103 | // mixed primitive type array 104 | const mixedArray = ['1', 2] // (string|number)[] 105 | let b = all(mixedArray).to(([one, two]) => { 106 | let arg0: string|number = one, arg1: string|number = two 107 | // guard disambiguates string|number 108 | if (typeof one === 'string') { 109 | one.toLowerCase() 110 | } else { 111 | // if not string, must be number 112 | let other: number = one 113 | one.toFixed() 114 | } 115 | if (typeof two === 'number') { 116 | two.toFixed() 117 | } else { 118 | // if not number, must be string 119 | let other: string = two 120 | two.toLowerCase() 121 | } 122 | }) 123 | } 124 | { 125 | // homogeneous variable array type 126 | const vnumberArray: Variable[] = [ 127 | new Variable(1), 128 | new Variable(new Variable(2)), 129 | new Variable(3).to(n => n*2) 130 | ] 131 | let a: Variable = all(vnumberArray) 132 | a.to(([one, two, three]) => { 133 | let n: number = one 134 | one.toFixed() 135 | two.toFixed() 136 | three.toFixed() 137 | }) 138 | // mixed variable type array 139 | const mixedArray = [new Variable('1'), new Variable(2)] // (Variable|Variable)[] 140 | let b = all(mixedArray).to(([one, two]) => { 141 | let arg0: string|number = one, arg1: string|number = two 142 | // guard disambiguates string|number 143 | if (typeof one === 'string') { 144 | one.toLowerCase() 145 | } else { 146 | // if not string, must be number 147 | let other: number = one 148 | one.toFixed() 149 | } 150 | if (typeof two === 'number') { 151 | two.toFixed() 152 | } else { 153 | // if not number, must be string 154 | let other: string = two 155 | two.toLowerCase() 156 | } 157 | }) 158 | } 159 | 160 | // VArray 161 | { 162 | const a = new VArray(['a','b']) 163 | const doubled: VariableClass = a.length.to(n => n < 1) 164 | const reduced = a.map(s => s.toLowerCase()).filter(s => s.split('/')).reduce((memo, s, i) => { 165 | memo[i] = s 166 | return memo 167 | }, { MEMO: true } as { [key: number]: string, MEMO: true }) 168 | const r = reduced.valueOf() 169 | const b: boolean = r.MEMO 170 | //r['0'] 171 | const s: string = r[0] 172 | const l: boolean = a.map(s => s.length).every(n => n < 2).valueOf() 173 | } 174 | // properties 175 | { 176 | let o = { 177 | str: 'string', 178 | num: 1, 179 | vb: new Variable(true), 180 | inner: new Variable({ 181 | x: 2, 182 | y: new Variable({ 183 | a: false 184 | }) 185 | }) 186 | } 187 | let vo = new Variable(o) 188 | const s: string = vo.get('str') 189 | const num: number = vo.get('num') 190 | const b: boolean = vo.get('vb') 191 | const inner = vo.get('inner') 192 | const x: number = inner.x 193 | //const y = inner.y 194 | //const yab: boolean = y.a 195 | let ps: Variable = vo.property('str') 196 | let pnum: Variable = vo.property('num') 197 | let pvb: Variable = vo.property('vb') 198 | let r = pvb.valueOf() 199 | let pvo = vo.property('inner') 200 | let pvoy = pvo.property('y') 201 | let avalue0: boolean = pvoy.get('a') 202 | let a = pvo.property('y').property('a') 203 | let avalue1: boolean = a.valueOf() 204 | 205 | vo.set('str', 123) // should fail type check 206 | } -------------------------------------------------------------------------------- /tests/unit.js: -------------------------------------------------------------------------------- 1 | if (typeof define === 'undefined') { define = function(module) { module(require) }} 2 | if (typeof assert === 'undefined') { assert = require('chai').assert } 3 | define(function(require) { 4 | require('./Variable') 5 | require('./Copy') 6 | require('./operators') 7 | require('./reactive') 8 | require('./ContextualPromise') 9 | }); 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2017"], 4 | "noEmit": true 5 | }, 6 | "files": ["index.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /util/ContextualPromise.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 2 | define(['./lang', '../Variable'], factory) } else if (typeof module === 'object' && module.exports) { 3 | module.exports = factory(require('./lang'), require('../Variable')) // Node 4 | }}(this, function (lang, VariableExports) { 5 | 6 | var Promise = (typeof global != 'undefined' ? global : window).Promise 7 | function ContextualPromise(executor) { 8 | this.context = VariableExports.currentContext 9 | this.promise = new Promise(executor) 10 | } 11 | lang.copy(ContextualPromise.prototype, { 12 | then: function(onFulfilled, onRejected) { 13 | var context = this.context 14 | var promise = this.promise 15 | if (context && promise.notifies) { 16 | var contextualPromise = this 17 | return context.executeWithin(function() { 18 | return new ContextualPromiseFromPromise(promise.then( 19 | getExecutor(onFulfilled, context), 20 | getExecutor(onRejected, context) 21 | ), context, contextualPromise) 22 | }) 23 | } 24 | return new ContextualPromiseFromPromise(promise.then( 25 | getExecutor(onFulfilled, context), 26 | getExecutor(onRejected, context) 27 | ), context, this) 28 | }, 29 | 'catch': function(onRejected) { 30 | var context = this.context 31 | return new ContextualPromiseFromPromise(this.promise.catch( 32 | getExecutor(onRejected, context) 33 | ), context, this) 34 | }, 35 | 'finally': function(onResolved) { 36 | var context = this.context 37 | return new ContextualPromiseFromPromise(this.promise.finally( 38 | getExecutor(onResolved, context) 39 | ), context, this) 40 | }, 41 | isContextual: true 42 | }) 43 | 44 | function getExecutor(executor, context) { 45 | if (executor && context) { 46 | return function(value) { 47 | return context.executeWithin(function() { 48 | var result = executor(value) 49 | if (result && result.then) { 50 | return new ContextualPromiseFromPromise(result, context) 51 | } 52 | return result 53 | }) 54 | } 55 | } 56 | return executor 57 | } 58 | ContextualPromise.resolve = function(value) { 59 | var promise = (value && value.then) ? value : Promise.resolve(value) 60 | return new ContextualPromiseFromPromise(promise, VariableExports.currentContext) 61 | } 62 | ContextualPromise.reject = function(value) { 63 | var promise = Promise.reject(value) 64 | return new ContextualPromiseFromPromise(promise, VariableExports.currentContext) 65 | } 66 | ContextualPromise.all = function(promises) { 67 | var startedPromises = startPromises(promises) 68 | var promise = new ContextualPromiseFromPromise(Promise.all(startedPromises), VariableExports.currentContext) 69 | if (startedPromises.assignAbort) { 70 | startedPromises.assignAbort(promise) 71 | } 72 | return promise 73 | } 74 | ContextualPromise.race = function(promises) { 75 | var startedPromises = startPromises(promises) 76 | var promise = new ContextualPromiseFromPromise(Promise.race(startedPromises), VariableExports.currentContext) 77 | if (startedPromises.assignAbort) { 78 | startedPromises.assignAbort(promise) 79 | } 80 | return promise 81 | } 82 | function startPromises(promises) { 83 | var contextualizedPromises = new Array(promises.length) 84 | var hasAbort = ContextualPromise.propagateAbort 85 | for (var i = 0, l = promises.length; i < l; i++) { 86 | var value = promises[i] 87 | if (value && value.then) { 88 | if (value.notifies) 89 | value = value.then() // execute then immediately (still using the current context) 90 | if (!value.abort) { 91 | hasAbort = false 92 | } 93 | } 94 | contextualizedPromises[i] = value 95 | } 96 | if (hasAbort) { 97 | contextualizedPromises.assignAbort = function(promise) { 98 | promise.abort = function(message) { 99 | for (var i = 0, l = promises.length; i < l; i++) { 100 | var promise = promises[i] 101 | if (promise && promise.abort) { 102 | promise.abort(message) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | return contextualizedPromises 109 | } 110 | function ContextualPromiseFromPromise(promise, context, originalPromise) { 111 | this.context = context 112 | this.promise = promise 113 | if (ContextualPromise.propagateAbort && (originalPromise || promise).abort) { 114 | this.abort = (originalPromise || promise).abort 115 | } 116 | } 117 | ContextualPromiseFromPromise.prototype = ContextualPromise.prototype 118 | return ContextualPromise 119 | })) 120 | -------------------------------------------------------------------------------- /util/lang.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { if (typeof define === 'function' && define.amd) { 2 | define([], factory) } else if (typeof module === 'object' && module.exports) { 3 | module.exports = factory() // Node 4 | }}(this, function () { 5 | var getPrototypeOf = Object.getPrototypeOf || (function(base) { return base.__proto__ }) 6 | var setPrototypeOf = Object.setPrototypeOf || (function(base, proto) { 7 | for (var key in proto) { 8 | try { 9 | if (!base.hasOwnProperty(key)) { 10 | if (proto.hasOwnProperty(key)) { 11 | Object.defineProperty(base, key, 12 | Object.getOwnPropertyDescriptor(proto, key)) 13 | } else { 14 | base[key] = proto[key] 15 | } 16 | } 17 | } catch(error) {} 18 | } 19 | }) 20 | var hasFeatures = { 21 | requestAnimationFrame: typeof requestAnimationFrame != 'undefined', 22 | defineProperty: Object.defineProperty && (function() { 23 | try{ 24 | Object.defineProperty({}, 't', {}) 25 | return true 26 | }catch(e) { 27 | } 28 | })(), 29 | promise: typeof Promise !== 'undefined', 30 | MutationObserver: typeof MutationObserver !== 'undefined', 31 | 'WeakMap': typeof WeakMap === 'function' 32 | } 33 | function has(feature) { 34 | return hasFeatures[feature] 35 | } 36 | 37 | function SyncPromise(value) { 38 | this.value = value 39 | } 40 | SyncPromise.prototype = { 41 | then: function(onFulfilled, onRejected) { 42 | if (!onFulfilled) { 43 | return new SyncPromise(this.value) 44 | } 45 | try { 46 | var result = onFulfilled(this.value) 47 | return (result && result.then) ? result : new SyncPromise(result) 48 | } catch(error) { 49 | return new SyncErrorPromise(error) 50 | } 51 | }, 52 | catch: function(handler) { 53 | return this.then(null, handler) 54 | }, 55 | finally: function(handler) { 56 | handler() 57 | return this 58 | } 59 | } 60 | function SyncErrorPromise(error) { 61 | this.value = error 62 | this.unhandledTimeout = setTimeout(function() { 63 | console.error('Uncaught (in promise)', error) 64 | }) 65 | } 66 | SyncErrorPromise.prototype = new SyncPromise() 67 | SyncErrorPromise.prototype.then = function(onFulfilled, onRejected) { 68 | clearTimeout(this.unhandledTimeout) 69 | if (!onRejected) { 70 | return new SyncErrorPromise(this.value) 71 | } 72 | return SyncPromise.prototype.then.call(this, onRejected) 73 | } 74 | // This is an polyfill for Object.observe with just enough functionality 75 | // for what Variables need 76 | // An observe function, with polyfile 77 | var observe = 78 | has('defineProperty') ? 79 | function observe(target, listener) { 80 | /*for(var i in target) { 81 | addKey(i) 82 | }*/ 83 | listener.addKey = addKey 84 | listener.remove = function() { 85 | listener = null 86 | } 87 | return listener 88 | function addKey(key) { 89 | var keyFlag = 'key' + key 90 | if(this[keyFlag]) { 91 | return 92 | }else{ 93 | this[keyFlag] = true 94 | } 95 | var currentValue = target[key] 96 | var targetAncestor = target 97 | var descriptor 98 | do { 99 | descriptor = Object.getOwnPropertyDescriptor(targetAncestor, key) 100 | } while(!descriptor && (targetAncestor = getPrototypeOf(targetAncestor))) 101 | 102 | if(descriptor && descriptor.set) { 103 | var previousSet = descriptor.set 104 | var previousGet = descriptor.get 105 | Object.defineProperty(target, key, { 106 | get: function() { 107 | return (currentValue = previousGet.call(this)) 108 | }, 109 | set: function(value) { 110 | previousSet.call(this, value) 111 | if(currentValue !== value) { 112 | currentValue = value 113 | if(listener) { 114 | listener([{target: this, name: key}]) 115 | } 116 | } 117 | }, 118 | enumerable: descriptor.enumerable 119 | }) 120 | }else{ 121 | Object.defineProperty(target, key, { 122 | get: function() { 123 | return currentValue 124 | }, 125 | set: function(value) { 126 | if(currentValue !== value) { 127 | currentValue = value 128 | if(listener) { 129 | listener([{target: this, name: key}]) 130 | } 131 | } 132 | }, 133 | enumerable: !descriptor || descriptor.enumerable 134 | }) 135 | } 136 | } 137 | } : 138 | // and finally a polling-based solution, for the really old browsers 139 | function(target, listener) { 140 | if(!timerStarted) { 141 | timerStarted = true 142 | setInterval(function() { 143 | for(var i = 0, l = watchedObjects.length; i < l; i++) { 144 | diff(watchedCopies[i], watchedObjects[i], listeners[i]) 145 | } 146 | }, 20) 147 | } 148 | var copy = {} 149 | for(var i in target) { 150 | if(target.hasOwnProperty(i)) { 151 | copy[i] = target[i] 152 | } 153 | } 154 | watchedObjects.push(target) 155 | watchedCopies.push(copy) 156 | listeners.push(listener) 157 | } 158 | var queuedListeners 159 | function queue(listener, object, name) { 160 | if(queuedListeners) { 161 | if(queuedListeners.indexOf(listener) === -1) { 162 | queuedListeners.push(listener) 163 | } 164 | }else{ 165 | queuedListeners = [listener] 166 | lang.nextTurn(function() { 167 | queuedListeners.forEach(function(listener) { 168 | var events = [] 169 | listener.properties.forEach(function(property) { 170 | events.push({target: listener.object, name: property}) 171 | }) 172 | listener(events) 173 | listener.object = null 174 | listener.properties = null 175 | }) 176 | queuedListeners = null 177 | }, 0) 178 | } 179 | listener.object = object 180 | var properties = listener.properties || (listener.properties = []) 181 | if(properties.indexOf(name) === -1) { 182 | properties.push(name) 183 | } 184 | } 185 | var unobserve = has('observe') ? Object.unobserve : 186 | function(target, listener) { 187 | if(listener.remove) { 188 | listener.remove() 189 | } 190 | for(var i = 0, l = watchedObjects.length; i < l; i++) { 191 | if(watchedObjects[i] === target && listeners[i] === listener) { 192 | watchedObjects.splice(i, 1) 193 | watchedCopies.splice(i, 1) 194 | listeners.splice(i, 1) 195 | return 196 | } 197 | } 198 | } 199 | var watchedObjects = [] 200 | var watchedCopies = [] 201 | var listeners = [] 202 | var timerStarted = false 203 | function diff(previous, current, callback) { 204 | // TODO: keep an array of properties for each watch for faster iteration 205 | var queued 206 | for(var i in previous) { 207 | if(previous.hasOwnProperty(i) && previous[i] !== current[i]) { 208 | // a property has changed 209 | previous[i] = current[i] 210 | (queued || (queued = [])).push({name: i}) 211 | } 212 | } 213 | for(var i in current) { 214 | if(current.hasOwnProperty(i) && !previous.hasOwnProperty(i)) { 215 | // a property has been added 216 | previous[i] = current[i] 217 | (queued || (queued = [])).push({name: i}) 218 | } 219 | } 220 | if(queued) { 221 | callback(queued) 222 | } 223 | } 224 | 225 | var id = 1 226 | // a function that returns a function, to stop JSON serialization of an 227 | // object 228 | function toJSONHidden() { 229 | return toJSONHidden 230 | } 231 | // An object that will be hidden from JSON serialization 232 | var Hidden = function () { 233 | } 234 | Hidden.prototype.toJSON = toJSONHidden 235 | 236 | var extendClass, constructOrCall 237 | try { 238 | // do this with an eval to avoid syntax errors in browsers that do not support class and new.target 239 | extendClass = eval('(function(Base){ return class extends Base {}})\n\n//# sourceURL=class-extend') 240 | var possibleConstructOrCall = eval('"use strict";(function(BaseClass, constructHandler, callHandler, constructClass){ return function Element() { return this instanceof Element ? constructHandler ? constructHandler.apply(new.target || this.constructor, arguments) : constructClass ? Reflect.construct(BaseClass, arguments, new.target || this.constructor) : lang.functionConstruct(BaseClass, arguments, new.target || this.constructor, this) : callHandler.apply(Element, arguments) } })\n\n//# sourceURL=construct-or-call') 241 | // actually using new.target bombs in Edge, so it is basically unusable 242 | new (possibleConstructOrCall(function() {}, function() {}))() 243 | constructOrCall = possibleConstructOrCall 244 | } catch(e) { 245 | } 246 | 247 | var lang = { 248 | rAFOrdered: 249 | (function() { 250 | var toRender = [] 251 | var queued = false 252 | function processAnimationFrame() { 253 | toRender.sort(function(a, b) { 254 | return (a.element.compareDocumentPosition(b.element) & 2) ? 1 : -1 255 | }) 256 | for (var i = 0; i < toRender.length; i++) { 257 | toRender[i]() 258 | } 259 | toRender = [] 260 | queued = false 261 | } 262 | function requestAnimationFrame(renderer, element) { 263 | renderer.element = element || document.body 264 | if (!queued) { 265 | (window.requestAnimationFrame || setTimeout)(processAnimationFrame) 266 | queued = true 267 | } 268 | toRender.push(renderer) 269 | } 270 | return requestAnimationFrame 271 | })(), 272 | SyncPromise: SyncPromise, 273 | SyncErrorPromise: SyncErrorPromise, 274 | Promise: has('promise') ? Promise : (function() { 275 | function Promise(execute) { 276 | var isResolved, resolution, errorResolution 277 | var queue = 0 278 | function resolve(value) { 279 | // resolve function 280 | if(value && value.then) { 281 | // received a promise, wait for it 282 | value.then(resolve, reject) 283 | }else{ 284 | resolution = value 285 | finished() 286 | } 287 | } 288 | function reject(error) { 289 | // reject function 290 | errorResolution = error 291 | finished() 292 | } 293 | execute(resolve, reject) 294 | function finished() { 295 | isResolved = true 296 | for(var i = 0, l = queue.length; i < l; i++) { 297 | queue[i]() 298 | } 299 | // clean out the memory 300 | queue = 0 301 | } 302 | return { 303 | then: function(callback, errback) { 304 | return new Promise(function(resolve, reject) { 305 | function handle() { 306 | // promise fulfilled, call the appropriate callback 307 | try{ 308 | if(errorResolution && !errback) { 309 | // errors without a handler flow through 310 | reject(errorResolution) 311 | }else{ 312 | // resolve to the callback's result 313 | resolve(errorResolution ? 314 | errback(errorResolution) : 315 | callback ? 316 | callback(resolution) : resolution) 317 | } 318 | }catch(newError) { 319 | // caught an error, reject the returned promise 320 | reject(newError) 321 | } 322 | } 323 | if(isResolved) { 324 | // already resolved, immediately handle 325 | handle() 326 | }else{ 327 | (queue || (queue = [])).push(handle) 328 | } 329 | }) 330 | } 331 | } 332 | } 333 | return Promise 334 | }()), 335 | Set: typeof Set !== 'undefined' ? 336 | new Set(['a']).length ? Set : 337 | function (elements) { // IE 11 doesn't support arguments to constructor 338 | var set = new Set() 339 | if (elements) { 340 | for (var i = 0; i < elements.length; i++) { 341 | set.add(elements[i]) 342 | } 343 | } 344 | return set 345 | } : 346 | function (elements) { 347 | elements = elements || [] 348 | return { 349 | add: function(element) { 350 | if (!this.has(element)) { 351 | elements.push(element) 352 | } 353 | }, 354 | has: function(element) { 355 | return elements.indexOf(element) > -1 356 | } 357 | } 358 | }, 359 | Map: typeof Map !== 'undefined' ? Map : function () { 360 | var map = Object.create(null) 361 | return { 362 | set: function(key, value) { 363 | map[key] = value 364 | }, 365 | has: function(element) { 366 | return Object.prototype.hasOwnProperty.call(map, key) 367 | }, 368 | get: function(key) { 369 | return map[key] 370 | } 371 | } 372 | }, 373 | WeakMap: has('WeakMap') ? WeakMap : 374 | function (values, name) { 375 | var mapProperty = '__' + (name || '') + id++ 376 | return has('defineProperty') ? 377 | { 378 | get: function (key) { 379 | return key[mapProperty] 380 | }, 381 | set: function (key, value) { 382 | Object.defineProperty(key, mapProperty, { 383 | value: value, 384 | enumerable: false 385 | }) 386 | } 387 | } : 388 | { 389 | get: function (key) { 390 | var intermediary = key[mapProperty] 391 | return intermediary && intermediary.value 392 | }, 393 | set: function (key, value) { 394 | // we use an intermediary that is hidden from JSON serialization, at least 395 | var intermediary = key[mapProperty] || (key[mapProperty] = new Hidden()) 396 | intermediary.value = value 397 | } 398 | } 399 | }, 400 | 401 | observe: observe, 402 | unobserve: unobserve, 403 | extendClass: extendClass, 404 | when: function(value, callback, errorHandler) { 405 | return value && value.then ? 406 | (value.then(callback, errorHandler) || value) : callback(value) 407 | }, 408 | compose: function(Base, constructor, properties) { 409 | var prototype = constructor.prototype = Object.create(Base.prototype) 410 | setPrototypeOf(constructor, Base) 411 | for(var i in properties) { 412 | prototype[i] = properties[i] 413 | } 414 | prototype.constructor = constructor 415 | return constructor 416 | }, 417 | setPrototypeOf: setPrototypeOf, 418 | nextTurn: has('MutationObserver') ? 419 | function (callback) { 420 | // promises don't resolve consistently on the next micro turn (Edge doesn't do it right), 421 | // so use mutation observer 422 | // TODO: make a faster mode that doesn't recreate each time 423 | var div = document.createElement('div') 424 | var observer = new MutationObserver(callback) 425 | observer.observe(div, { 426 | attributes: true 427 | }) 428 | div.setAttribute('a', id++) 429 | } : 430 | function (callback) { 431 | // TODO: we can do better for other, older browsers 432 | setTimeout(callback, 0) 433 | }, 434 | copy: Object.assign || function(target, source) { 435 | for(var i in source) { 436 | target[i] = source[i] 437 | } 438 | return target 439 | }, 440 | deepCopy: function(source) { 441 | if (source && typeof source == 'object') { 442 | if (source instanceof Array) { 443 | var target = [] // always create a new array for array targets 444 | for(var i = 0, l = source.length; i < l; i++) { 445 | target[i] = lang.deepCopy(source[i]) 446 | } 447 | } else { 448 | var target = {} 449 | for (var i in source) { 450 | target[i] = lang.deepCopy(source[i]) 451 | } 452 | } 453 | return target 454 | } 455 | return source 456 | }, 457 | constructOrCall: constructOrCall || function(BaseClass, constructHandler, callHandler) { 458 | return function Element() { 459 | if (this instanceof Element) { 460 | if (!this.hasOwnProperty('constructor') && Element.prototype === getPrototypeOf(this)) { 461 | if (constructHandler) { 462 | return constructHandler.apply(Element, arguments) 463 | } 464 | if (lang.buggyConstructorSetter) { 465 | // in safari, directly setting the constructor messes up the native prototype 466 | Object.defineProperty(this, 'constructor', { value: Element }) 467 | } else { 468 | this.constructor = Element 469 | } 470 | } else if (constructHandler) { 471 | return constructHandler.apply(this.constructor, arguments) 472 | } 473 | return BaseClass.apply(this, arguments) 474 | } else { 475 | return callHandler.apply(Element, arguments) 476 | } 477 | } 478 | }, 479 | functionConstruct: function(BaseClass, args, SubClass, instance) { 480 | if (!instance.hasOwnProperty('constructor') && SubClass.prototype === Object.getPrototypeOf(instance)) { 481 | instance = Object.create(SubClass.prototype) 482 | if (lang.buggyConstructorSetter) { 483 | // in safari, directly setting the constructor messes up the native prototype 484 | Object.defineProperty(instance, 'constructor', { value: SubClass }) 485 | } else { 486 | instance.constructor = SubClass 487 | } 488 | } 489 | return BaseClass.apply(instance, args) 490 | } 491 | } 492 | function isGenerator(func) { 493 | if (typeof func === 'function') { 494 | var constructor = func.constructor 495 | // this is used to handle both native generators and transpiled generators 496 | return (constructor.displayName || constructor.name) === 'GeneratorFunction' 497 | } 498 | } 499 | function isGeneratorIterator(iterator) { 500 | if (iterator && iterator.next) { 501 | var constructor = iterator.constructor.constructor 502 | return (constructor.displayName || constructor.name) === 'GeneratorFunction' 503 | } 504 | } 505 | lang.isGenerator = isGenerator 506 | 507 | function spawn(generator) { 508 | var generatorIterator = typeof generator === 'function' ? generator() : generator 509 | var resuming 510 | var nextValue 511 | var isThrowing 512 | return next() 513 | function next() { 514 | do { 515 | var stepReturn = generatorIterator[isThrowing ? 'throw' : 'next'](nextValue) 516 | if (stepReturn.done) { 517 | return stepReturn.value 518 | } 519 | nextValue = stepReturn.value 520 | try { 521 | // if the return value is a (generator) iterator, execute it 522 | if (nextValue && nextValue.next && isGeneratorIterator(nextValue)) { 523 | nextValue = spawn(nextValue) 524 | } 525 | if (nextValue && nextValue.then) { 526 | // if it is a promise, we will wait on it 527 | // and return the promise so that the next caller can wait on this 528 | var resolved 529 | var isSync = null 530 | var result = nextValue.then(function(value) { 531 | nextValue = value 532 | isThrowing = false 533 | if (isSync === false) { 534 | return next() 535 | } else { 536 | isSync = true 537 | } 538 | }, function(error) { 539 | nextValue = error 540 | isThrowing = true 541 | return next() 542 | }) 543 | if (!isSync) { 544 | isSync = false 545 | return result 546 | } // else keeping looping to avoid recursion 547 | } 548 | isThrowing = false 549 | } catch(error) { 550 | isThrowing = true 551 | nextValue = error 552 | } 553 | } while(true) 554 | } 555 | } 556 | lang.spawn = spawn 557 | return lang 558 | })) 559 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | module.exports = { 3 | entry: './index.js', 4 | output: { 5 | filename: './index.js', 6 | library: 'alkali', 7 | libraryTarget: 'umd' 8 | }, 9 | optimization: { 10 | minimize: true 11 | }, 12 | mode: 'production', 13 | devtool: 'source-map' 14 | }; 15 | --------------------------------------------------------------------------------