├── .gitignore
├── bower.json
├── assets
├── powers-dre.png
├── alkali-logo-old.png
└── alkali-logo.svg
├── tests
├── has.js
├── all.js
├── tsconfig.json
├── unit.js
├── form.html
├── test.html
├── ContextualPromise.js
├── Copy.js
├── operators.js
├── reactive.js
├── intern.js
├── dstore.js
├── dgrid.js
├── types.ts
├── Renderer.js
├── es6.js
└── Element.js
├── tsconfig.json
├── .travis.yml
├── webpack.config.js
├── package.json
├── package.js
├── LICENSE
├── extensions
├── typescript.js
├── async-hooks.js
└── dstore.js
├── index.js
├── .jshintrc
├── bin
└── makeProperties.js
├── Copy.js
├── operators.js
├── util
├── ContextualPromise.js
└── lang.js
├── reactive.js
├── Renderer.js
└── index.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alkali",
3 | "main": "dist/index.js"
4 | }
5 |
--------------------------------------------------------------------------------
/assets/powers-dre.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kriszyp/alkali/master/assets/powers-dre.png
--------------------------------------------------------------------------------
/assets/alkali-logo-old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kriszyp/alkali/master/assets/alkali-logo-old.png
--------------------------------------------------------------------------------
/tests/has.js:
--------------------------------------------------------------------------------
1 | define(['dojo/has'], function(has) {
2 | has.add('promise', !!window.Promise)
3 | return has
4 | })
5 |
--------------------------------------------------------------------------------
/tests/all.js:
--------------------------------------------------------------------------------
1 | define([
2 | './unit',
3 | './Renderer',
4 | './Element',
5 | './dstore',
6 | './es6'
7 | ], function () {
8 | });
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["DOM", "ES2017"],
4 | "noEmit": true
5 | },
6 | "files": ["index.d.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/form.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Alkali Form
6 |
7 |
8 |
9 | Litmus Link
10 |
11 |
12 |
13 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
17 |
18 |
19 |
30 |
31 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alkali",
3 | "author": "Kris Zyp",
4 | "version": "1.1.4",
5 | "description": "Reactivity with native JavaScript objects and HTML elements",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "http://github.com/kriszyp/alkali"
10 | },
11 | "scripts": {
12 | "build": "webpack",
13 | "test": "./node_modules/.bin/mocha tests/unit -u tdd",
14 | "test.types": "./node_modules/.bin/tsc -p tests"
15 | },
16 | "main": "./index.js",
17 | "dependencies": {},
18 | "types": "./index.d.ts",
19 | "dojoBuild": "package.js",
20 | "devDependencies": {
21 | "bluebird": "3.5",
22 | "dojo": "^1.10.2",
23 | "dojo-dstore": "^1.1.1",
24 | "mocha": "^6.1",
25 | "chai": "^4.2",
26 | "webpack": "^4.33.0",
27 | "webpack-cli": "^3.3.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | var miniExcludes = {
2 | "xstyle/index.html": 1,
3 | "xstyle/README.md": 1,
4 | "xstyle/package": 1
5 | },
6 | copyOnlyRe = [
7 | /\/build/, // contents of build folder and build.js
8 | /\/core\/amdLoader/, // contents of core folder
9 | /\/core\/put/, // contents of core folder
10 | /\/xstyle(\.min)?$/ // xstyle.min.*
11 | ],
12 | isTestRe = /\/tests\//;
13 |
14 | var profile = {
15 | resourceTags: {
16 | test: function(filename, mid){
17 | return isTestRe.test(filename);
18 | },
19 |
20 | miniExclude: function(filename, mid){
21 | return isTestRe.test(filename) || mid in miniExcludes;
22 | },
23 |
24 | amd: function(filename, mid){
25 | return /\.js$/.test(filename);
26 | },
27 |
28 | copyOnly: function(filename, mid){
29 | for(var i = copyOnlyRe.length; i--;){
30 | if(copyOnlyRe[i].test(mid)){
31 | return true;
32 | }
33 | }
34 | return false;
35 | }
36 | }
37 | };
--------------------------------------------------------------------------------
/tests/ContextualPromise.js:
--------------------------------------------------------------------------------
1 | define(function(require) {
2 | require('bluebird/js/browser/bluebird')
3 | var VariableExports = require('../Variable')
4 | var ContextualPromise = require('../util/ContextualPromise')
5 | var Variable = VariableExports.Variable
6 | var Context = VariableExports.Context
7 | suite('Contextual Promise', function() {
8 |
9 | test('preserve context', function() {
10 | var context = new Context({ flag: true })
11 | var MyVariable = Variable({})
12 | MyVariable.prototype.valueOf = function() {
13 | var myContext = VariableExports.currentContext
14 | return myContext
15 | }
16 | var myVar = new MyVariable()
17 | assert.equal(myVar.valueOf(), undefined)
18 | var promise = context.executeWithin(function() {
19 | assert.isTrue(myVar.valueOf().subject.flag)
20 | return new ContextualPromise(function(resolved) {
21 | setTimeout(resolved)
22 | }).then(function() {
23 | assert.isTrue(myVar.valueOf().subject.flag)
24 | })
25 | })
26 | assert.equal(myVar.valueOf(), undefined)
27 | return promise
28 | })
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/assets/alkali-logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/tests/Copy.js:
--------------------------------------------------------------------------------
1 | define(function(require) {
2 | var VariableExports = require('../Variable')
3 | var Copy = require('../Copy')
4 | var Variable = VariableExports.Variable
5 | suite('Copy Variable', function() {
6 |
7 | test('simple single value', function() {
8 | var invalidated = false;
9 | var variable = new Variable({
10 | a: 1,
11 | b: 2
12 | });
13 | var a = variable.property('a');
14 | var copy = new Copy(variable);
15 | var copyA = copy.property('a');
16 | var invalidated = false;
17 | a.notifies({
18 | updated: function(){
19 | invalidated = true;
20 | }
21 | });
22 | var copyInvalidated = false;
23 | copyA.notifies({
24 | updated: function(){
25 | copyInvalidated = true;
26 | }
27 | });
28 | copyA.put('4');
29 | assert.isTrue(copyInvalidated);
30 | assert.isFalse(invalidated);
31 | copy.save();
32 | assert.isTrue(invalidated);
33 | assert.equal(a.valueOf(), 4);
34 | })
35 | test('nested object', function() {
36 | var invalidated = false;
37 | var variable = new Variable({
38 | a: 1,
39 | b: {
40 | c: 3
41 | }
42 | });
43 | var c = variable.property('b').property('c');
44 | var copy = new Copy(variable);
45 | var copyC = copy.property('b').property('c');
46 | var invalidated = false;
47 | c.notifies({
48 | updated: function(){
49 | invalidated = true;
50 | }
51 | });
52 | var copyInvalidated = false;
53 | copyC.notifies({
54 | updated: function(){
55 | copyInvalidated = true;
56 | }
57 | });
58 | copyC.put('4');
59 | assert.isTrue(copyInvalidated);
60 | assert.isFalse(invalidated);
61 | copy.save();
62 | assert.isTrue(invalidated);
63 | assert.equal(c.valueOf(), 4);
64 | })
65 |
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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
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/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 |
--------------------------------------------------------------------------------
/reactive.js:
--------------------------------------------------------------------------------
1 | (function (root, factory) { if (typeof define === 'function' && define.amd) {
2 | define(['./util/lang', './operators', './Variable'], factory) } else if (typeof module === 'object' && module.exports) {
3 | module.exports = factory(require('./util/lang'), require('./operators'), require('./Variable')) // Node
4 | }}(this, function (lang, operators, VariableExports) {
5 |
6 | var Transform = VariableExports.Transform
7 | var Variable = VariableExports.Variable
8 | var changeValue = VariableExports._changeValue
9 | var VArray = VariableExports.VArray
10 | var isGenerator = lang.isGenerator
11 | var ObjectTransform = lang.compose(Transform, function ObjectTransform(source, transform, sources) {
12 | this.sources = sources
13 | Transform.apply(this, arguments)
14 | }, {
15 | _getAsObject: function() {
16 | return this.transform.apply(this, preserveObjects(this.sources))
17 | }
18 | })
19 | function preserveObjects(sources) {
20 | for (var i = 0, l = sources.length; i < l; i++) {
21 | var source = sources[i]
22 | if (source && source._getAsObject) {
23 | sources[i] = source._getAsObject()
24 | }
25 | }
26 | return sources
27 | }
28 |
29 | var typeMappings = new Map()
30 | typeMappings.set('string', VariableExports.VString)
31 | typeMappings.set('number', VariableExports.VNumber)
32 | typeMappings.set('boolean', VariableExports.VBoolean)
33 | typeMappings.set('undefined', Variable)
34 | typeMappings.set(null, Variable)
35 | typeMappings.set(Array, VArray)
36 | typeMappings.set(Map, VariableExports.VMap)
37 | typeMappings.set(Set, VariableExports.VSet)
38 | function reactive(value) {
39 | return fromValue(value)
40 | }
41 | VariableExports.reactive = reactive
42 | function fromValue(value, isProperty, deferObject) {
43 |
44 | // get the type for primitives or known constructors (or null)
45 | var Type = typeMappings.get(typeof value) || typeMappings.get(value && value.constructor) || value instanceof Variable && !(value instanceof Transform) && value.constructor
46 | if (Type) {
47 | return new Type(isProperty ? undefined : value)
48 | }
49 | if (deferObject) {
50 | return
51 | }
52 | // an object
53 | var objectVar
54 | if (isProperty) {
55 | objectVar = new Variable()
56 | } else {
57 | objectVar = new Variable(value)
58 | if (value && typeof value == 'object') {
59 | // by default, if given an object (or variable or array), treat it as fixed
60 | objectVar.fixed = true
61 | }
62 | }
63 | if (value.then) {
64 | // incoming promise or variable, just as a proxy
65 | return objectVar
66 | }
67 | for (var key in value) {
68 | if (value.hasOwnProperty(key)) {
69 | var propertyValue = value[key]
70 | var propertyVariable = fromValue(propertyValue, true, true)
71 | if (propertyVariable) {
72 | propertyVariable.key = key
73 | propertyVariable.parent = objectVar
74 | if (objectVar[key] === undefined) {
75 | objectVar[key] = propertyVariable
76 | } else {
77 | (objectVar._properties || (objectVar._properties = {}))[key] = propertyVariable
78 | }
79 | } else {
80 | // deferred, use getter/setter
81 | defineValueProperty(objectVar, key, value)
82 | }
83 | }
84 | }
85 | return objectVar
86 | }
87 |
88 |
89 | function defineValueProperty(target, key, object) {
90 | var Type
91 | Object.defineProperty(target, key, {
92 | get: function() {
93 | return reactive.get(this, key, function() { return fromValue(object[key], true) })
94 | },
95 | set: function(value) {
96 | reactive.set(this, key, value)
97 | },
98 | enumerable: true
99 | })
100 | }
101 | lang.copy(reactive, {
102 | from: function(value, options) {
103 | if (value && value.property) {
104 | return value
105 | }
106 | if (typeof value === 'function' && isGenerator(value)) {
107 | return VariableExports.react(value, options)
108 | }
109 | return Variable.from(value)
110 | },
111 | getp: function(object, property) {
112 | if (object) {
113 | // TODO: Use a static set of public methods/properties that can be accessed
114 | if (object.property) {
115 | // it is a variable already, but check to see if we are using a method/property directly on the variable
116 | var directPropertyValue = object[property]
117 | return directPropertyValue !== undefined ? directPropertyValue : object.property(property)
118 | }
119 | return object[property]
120 | }
121 | // not even truthy, return undefined
122 | },
123 | get: function(target, key, Type) { // for TS/Babel decorators
124 | var property = (target._properties || (target._properties = {}))[key]
125 | if (!property) {
126 | target._properties[key] = property = new (getType(Type))()
127 | if (target.getValue) {
128 | property.key = key
129 | property.parent = target
130 | if (property.listeners) {
131 | // if it already has listeners, need to reinit it with the parent
132 | property.init()
133 | }
134 | }
135 | }
136 | return property
137 | },
138 | set: function(target, key, value) {
139 | var property = target[key]
140 | property.parent ? changeValue(property, 3, value) : property.put(value)
141 | },
142 | prop: function(Type) {
143 | return function(prototype, key) {
144 | defineProperty(prototype, key, Type)
145 | }
146 | },
147 | cond: function(test, consequent, alternate) {
148 | return operators.if(test, operators.choose(consequent, alternate))
149 | },
150 | fcall: function(target, args) {
151 | if (target.property && typeof target === 'function') {
152 | return target.apply(null, preserveObjects(args))
153 | }
154 | return new Transform(args[0], target, args)
155 | },
156 | mcall: function(target, key, args) {
157 | var method = target[key]
158 | if (typeof method === 'function' && method.property || key === 'bind') {
159 | // for now we check to see if looks like it could handle a variable, or is a bind call
160 | return method.apply(target, preserveObjects(args))
161 | }
162 | return new Transform(args[0], target[key].bind(target), args)
163 | },
164 | ncall: function(target, args) {
165 | if (target.property && typeof target === 'function') {
166 | return new (target.bind.apply(target, [null].concat(preserveObjects(args))))()
167 | }
168 | return new Transform(args[0], function() {
169 | return new (target.bind.apply(target, [null].concat(arguments)))()
170 | }, args)
171 | },
172 |
173 | obj: function(sources) {
174 | return sources
175 | //return new ObjectTransform(sources[0], transform, sources)
176 | },
177 |
178 | val: function(value) {
179 | return value && value.valueOf()
180 | },
181 | cls: function(definitions) {
182 | reactive = this // TODO: clean this up
183 | return function(Class) {
184 | var prototype = Class.prototype
185 | if (!(prototype instanceof Variable)) {
186 | if (Object.getPrototypeOf(prototype) == Object.prototype) {
187 | Object.setPrototypeOf(Class, Variable)
188 | Object.setPrototypeOf(prototype, Variable.prototype)
189 | } else {
190 | console.warn('Unable to make class a reactive variable')
191 | }
192 | }
193 | for (var key in definitions) {
194 | defineProperty(prototype, key, definitions[key])
195 | }
196 | }
197 | }
198 | })
199 | lang.copy(reactive, operators)
200 |
201 | function getType(Type) {
202 | if (typeMappings.has(Type)) {
203 | return typeMappings.get(Type)
204 | } else if (typeof Type === 'object') {
205 | if (Type instanceof Array) {
206 | if (Type[0]) {
207 | return VArray.of(getType(Type[0]))
208 | } else {
209 | return VArray
210 | }
211 | }
212 | var typedObject = {}
213 | var hasKeys
214 | for (var key in Type) {
215 | typedObject[key] = getType(Type[key])
216 | hasKeys = true
217 | }
218 | return hasKeys ? Variable.with(typedObject) : Variable
219 | }
220 | return Type
221 | }
222 |
223 | function defineProperty(target, key, Type) {
224 | if (!Type) {
225 | console.warn('Invalid type specified for', target && target.constructor.name, 'property', key, '(ensure you are using a concrete type, not an interface)')
226 | } else if (!Type.notifies) {
227 | Type = getType(Type) || Variable
228 | }
229 | Object.defineProperty(target, key, {
230 | get: function() {
231 | return reactive.get(this, key, Type)
232 | },
233 | set: function(value) {
234 | reactive.set(this, key, value)
235 | },
236 | enumerable: true
237 | })
238 | }
239 |
240 | return reactive
241 | }))
242 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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