Commit 4ae9282d39a3f4b2b43760e4c33d0408c400aa04
initial code dump!
Matt McKegg committed on 7/7/2016, 5:31:08 AMFiles changed
.gitignore | added |
README.md | added |
array.js | added |
computed.js | added |
html-element.js | added |
is-observable.js | added |
lib/apply-properties.js | added |
lib/parse-tag.js | added |
map.js | added |
package.json | added |
resolve.js | added |
send.js | added |
set.js | added |
struct.js | added |
test/dom.js | added |
test/test-array.js | added |
test/test-map.js | added |
test/test-set.js | added |
test/test-struct.js | added |
test/test-varhash.js | added |
transforms/flatten.js | added |
transforms/lookup.js | added |
transforms/sort.js | added |
transforms/uniq.js | added |
value.js | added |
varhash.js | added |
watch.js | added |
README.md | ||
---|---|---|
@@ -1,0 +1,63 @@ | ||
1 … | +mutant | |
2 … | +=== | |
3 … | + | |
4 … | +Create observables and map them to DOM elements. Massively inspired by `hyperscript` and `observ-*`. | |
5 … | + | |
6 … | +No virtual dom, just direct observable bindings. Unnecessary garbage collection is avoided by using mutable objects instead of blasting immutable junk all over the place. | |
7 … | + | |
8 … | +## Current Status: Experimental / Unpublished | |
9 … | + | |
10 … | +## Install | |
11 … | + | |
12 … | +```bash | |
13 … | +npm install @mmckegg/mutant --save | |
14 … | +``` | |
15 … | + | |
16 … | +## Use | |
17 … | + | |
18 … | +```js | |
19 … | +var Struct = require('@mmckegg/mutant/struct') | |
20 … | +var send = require('@mmckegg/mutant/send') | |
21 … | +var h = require('@mmckegg/mutant/html-element') | |
22 … | + | |
23 … | +var state = Struct({ | |
24 … | + text: 'Test', | |
25 … | + color: 'red', | |
26 … | + value: 0 | |
27 … | +}) | |
28 … | + | |
29 … | +var element = h('div.cool', { | |
30 … | + class: ['cool', state.text], | |
31 … | + style: { | |
32 … | + 'background-color': state.color | |
33 … | + } | |
34 … | +}, [ | |
35 … | + h('div', [ | |
36 … | + state.text, ' ', state.value, ' ', h('strong', 'test') | |
37 … | + ]), | |
38 … | + h('div', [ | |
39 … | + h('button', { | |
40 … | + 'ev-click': send(state.color.set, 'blue') | |
41 … | + }, 'Change color') | |
42 … | + ]) | |
43 … | +]) | |
44 … | + | |
45 … | +setTimeout(function () { | |
46 … | + state.text.set('Another value') | |
47 … | +}, 5000) | |
48 … | + | |
49 … | +setInterval(function () { | |
50 … | + state.value.set(state.value() + 1) | |
51 … | +}, 1000) | |
52 … | + | |
53 … | +setInterval(function () { | |
54 … | + // bulk update state | |
55 … | + state.set({ | |
56 … | + text: 'Retrieved from server (not really)', | |
57 … | + color: '#FFEECC', | |
58 … | + value: 1337 | |
59 … | + }) | |
60 … | +}, 10000) | |
61 … | + | |
62 … | +document.body.appendChild(element) | |
63 … | +``` |
array.js | ||
---|---|---|
@@ -1,0 +1,126 @@ | ||
1 … | +var Value = require('./value') | |
2 … | + | |
3 … | +module.exports = Array | |
4 … | + | |
5 … | +function Array (defaultValues) { | |
6 … | + var object = [] | |
7 … | + var sources = [] | |
8 … | + var releases = [] | |
9 … | + | |
10 … | + if (defaultValues && defaultValues.length) { | |
11 … | + defaultValues.forEach(add) | |
12 … | + } | |
13 … | + | |
14 … | + var observable = Value(object) | |
15 … | + var broadcast = observable.set | |
16 … | + | |
17 … | + observable.push = function (args) { | |
18 … | + for (var i = 0; i < arguments.length; i++) { | |
19 … | + add(arguments[i]) | |
20 … | + } | |
21 … | + broadcast(object) | |
22 … | + } | |
23 … | + | |
24 … | + observable.insert = function (valueOrObs, at) { | |
25 … | + sources.splice(at, 0, valueOrObs) | |
26 … | + releases.splice(at, 0, bind(valueOrObs)) | |
27 … | + object.splice(at, 0, resolve(valueOrObs)) | |
28 … | + broadcast(object) | |
29 … | + } | |
30 … | + | |
31 … | + observable.get = function (index) { | |
32 … | + return sources[index] | |
33 … | + } | |
34 … | + | |
35 … | + observable.getLength = function (index) { | |
36 … | + return sources.length | |
37 … | + } | |
38 … | + | |
39 … | + observable.includes = function (valueOrObs) { | |
40 … | + return !!~sources.indexOf(valueOrObs) | |
41 … | + } | |
42 … | + | |
43 … | + observable.indexOf = function (valueOrObs) { | |
44 … | + return sources.indexOf(valueOrObs) | |
45 … | + } | |
46 … | + | |
47 … | + observable.indexOf | |
48 … | + | |
49 … | + observable.pop = function () { | |
50 … | + var result = sources.pop() | |
51 … | + tryInvoke(releases.pop()) | |
52 … | + object.pop() | |
53 … | + broadcast(object) | |
54 … | + return result | |
55 … | + } | |
56 … | + | |
57 … | + observable.shift = function () { | |
58 … | + var result = sources.shift() | |
59 … | + tryInvoke(releases.shift()) | |
60 … | + object.shift() | |
61 … | + broadcast(object) | |
62 … | + return result | |
63 … | + } | |
64 … | + | |
65 … | + observable.clear = function () { | |
66 … | + releases.forEach(tryInvoke) | |
67 … | + sources.length = 0 | |
68 … | + releases.length = 0 | |
69 … | + object.length = 0 | |
70 … | + broadcast(object) | |
71 … | + } | |
72 … | + | |
73 … | + observable.delete = function (valueOrObs) { | |
74 … | + var index = sources.indexOf(valueOrObs) | |
75 … | + if (~index) { | |
76 … | + sources.splice(index, 1) | |
77 … | + releases.splice(index, 1).forEach(tryInvoke) | |
78 … | + object.splice(index, 1) | |
79 … | + broadcast(object) | |
80 … | + } | |
81 … | + } | |
82 … | + | |
83 … | + observable.set = function (values) { | |
84 … | + releases.forEach(tryInvoke) | |
85 … | + sources.length = 0 | |
86 … | + releases.length = 0 | |
87 … | + object.length = 0 | |
88 … | + values.forEach(add) | |
89 … | + broadcast(object) | |
90 … | + } | |
91 … | + | |
92 … | + observable.destroy = observable.clear | |
93 … | + | |
94 … | + return observable | |
95 … | + | |
96 … | + // scoped | |
97 … | + | |
98 … | + function add (valueOrObs) { | |
99 … | + sources.push(valueOrObs) | |
100 … | + releases.push(bind(valueOrObs)) | |
101 … | + object.push(resolve(valueOrObs)) | |
102 … | + } | |
103 … | + | |
104 … | + function bind (valueOrObs) { | |
105 … | + return typeof valueOrObs === 'function' ? valueOrObs(update.bind(this, valueOrObs)) : null | |
106 … | + } | |
107 … | + | |
108 … | + function update (obs, value) { | |
109 … | + sources.forEach(function (source, i) { | |
110 … | + if (source === obs) { | |
111 … | + object[i] = value | |
112 … | + } | |
113 … | + }) | |
114 … | + broadcast(object) | |
115 … | + } | |
116 … | +} | |
117 … | + | |
118 … | +function resolve (source) { | |
119 … | + return typeof source === 'function' ? source() : source | |
120 … | +} | |
121 … | + | |
122 … | +function tryInvoke (func) { | |
123 … | + if (typeof func === 'function') { | |
124 … | + func() | |
125 … | + } | |
126 … | +} |
computed.js | ||
---|---|---|
@@ -1,0 +1,102 @@ | ||
1 … | +/* A lazy binding take on computed */ | |
2 … | +// doesn't start watching observables until itself is watched, and then releases if unwatched | |
3 … | +// avoids memory/watcher leakage | |
4 … | + | |
5 … | +var resolve = require('./resolve') | |
6 … | + | |
7 … | +module.exports = computed | |
8 … | + | |
9 … | +function computed (observables, lambda) { | |
10 … | + var values = [] | |
11 … | + var releases = [] | |
12 … | + var computedValue = null | |
13 … | + var live = false | |
14 … | + var lazy = false | |
15 … | + var listeners = [] | |
16 … | + | |
17 … | + var result = function (listener) { | |
18 … | + if (!listener) { | |
19 … | + return getValue() | |
20 … | + } | |
21 … | + | |
22 … | + if (typeof listener !== 'function') { | |
23 … | + throw new Error('Listeners must be functions.') | |
24 … | + } | |
25 … | + | |
26 … | + listeners.push(listener) | |
27 … | + listen() | |
28 … | + | |
29 … | + return function remove () { | |
30 … | + for (var i = 0, len = listeners.length; i < len; i++) { | |
31 … | + if (listeners[i] === listener) { | |
32 … | + listeners.splice(i, 1) | |
33 … | + break | |
34 … | + } | |
35 … | + } | |
36 … | + if (!listeners.length) { | |
37 … | + unlisten() | |
38 … | + } | |
39 … | + } | |
40 … | + } | |
41 … | + | |
42 … | + return result | |
43 … | + | |
44 … | + // scoped | |
45 … | + | |
46 … | + function listen () { | |
47 … | + if (!live) { | |
48 … | + for (var i = 0, len = observables.length; i < len; i++) { | |
49 … | + if (typeof observables[i] === 'function') { | |
50 … | + releases.push(observables[i](onUpdate)) | |
51 … | + } | |
52 … | + } | |
53 … | + live = true | |
54 … | + lazy = true | |
55 … | + } | |
56 … | + } | |
57 … | + | |
58 … | + function unlisten () { | |
59 … | + if (live) { | |
60 … | + live = false | |
61 … | + while (releases.length) { | |
62 … | + releases.pop()() | |
63 … | + } | |
64 … | + } | |
65 … | + } | |
66 … | + | |
67 … | + function update () { | |
68 … | + var changed = false | |
69 … | + for (var i = 0, len = observables.length; i < len; i++) { | |
70 … | + var newValue = resolve(observables[i]) | |
71 … | + if (newValue !== values[i] || typeof newValue === 'object') { | |
72 … | + changed = true | |
73 … | + values[i] = newValue | |
74 … | + } | |
75 … | + } | |
76 … | + | |
77 … | + if (changed) { | |
78 … | + var newComputedValue = lambda.apply(null, values) | |
79 … | + if (newComputedValue !== computedValue || typeof newComputedValue === 'object') { | |
80 … | + computedValue = newComputedValue | |
81 … | + return true | |
82 … | + } | |
83 … | + } | |
84 … | + return false | |
85 … | + } | |
86 … | + | |
87 … | + function onUpdate () { | |
88 … | + if (update()) { | |
89 … | + for (var i = 0, len = listeners.length; i < len; i++) { | |
90 … | + listeners[i](computedValue) | |
91 … | + } | |
92 … | + } | |
93 … | + } | |
94 … | + | |
95 … | + function getValue () { | |
96 … | + if (!live || lazy) { | |
97 … | + lazy = false | |
98 … | + update() | |
99 … | + } | |
100 … | + return computedValue | |
101 … | + } | |
102 … | +} |
html-element.js | ||
---|---|---|
@@ -1,0 +1,154 @@ | ||
1 … | +var applyProperties = require('./lib/apply-properties') | |
2 … | +var isObservable = require('./is-observable') | |
3 … | +var parseTag = require('./lib/parse-tag') | |
4 … | +var caches = new global.WeakMap() | |
5 … | + | |
6 … | +module.exports = function (tag, attributes, children) { | |
7 … | + return Element(global.document, null, tag, attributes, children) | |
8 … | +} | |
9 … | + | |
10 … | +module.exports.forDocument = function (document, namespace) { | |
11 … | + return Element.bind(this, document, namespace) | |
12 … | +} | |
13 … | + | |
14 … | +module.exports.destroy = function (node) { | |
15 … | + var data = caches.get(node) | |
16 … | + if (data) { | |
17 … | + Array.from(data.releases.values()).forEach(tryInvoke) | |
18 … | + caches.delete(node) | |
19 … | + } | |
20 … | + applyProperties.destroy(node) | |
21 … | +} | |
22 … | + | |
23 … | +function Element (document, namespace, tagName, properties, children) { | |
24 … | + if (!children && (Array.isArray(properties) || isText(properties))) { | |
25 … | + children = properties | |
26 … | + properties = null | |
27 … | + } | |
28 … | + | |
29 … | + properties = properties || {} | |
30 … | + | |
31 … | + var tag = parseTag(tagName, properties, namespace) | |
32 … | + var node = namespace | |
33 … | + ? document.createElementNS(namespace, tag.tagName) | |
34 … | + : document.createElement(tag.tagName) | |
35 … | + | |
36 … | + if (!namespace) { | |
37 … | + if (tag.id) { | |
38 … | + node.id = tag.id | |
39 … | + } | |
40 … | + if (tag.classes && tag.classes.length) { | |
41 … | + node.className = tag.classes.join(' ') | |
42 … | + } | |
43 … | + } | |
44 … | + | |
45 … | + var data = { | |
46 … | + targets: new Map(), | |
47 … | + releases: new Map() | |
48 … | + } | |
49 … | + | |
50 … | + caches.set(node, data) | |
51 … | + applyProperties(node, properties, namespace) | |
52 … | + if (children != null) { | |
53 … | + appendChild(document, node, data, children) | |
54 … | + } | |
55 … | + | |
56 … | + return node | |
57 … | +} | |
58 … | + | |
59 … | +function appendChild (document, target, data, node) { | |
60 … | + if (Array.isArray(node)) { | |
61 … | + node.forEach(function (child) { | |
62 … | + appendChild(document, target, data, child) | |
63 … | + }) | |
64 … | + } else if (isObservable(node)) { | |
65 … | + var nodes = getNodes(document, resolve(node)) | |
66 … | + nodes.forEach(append, target) | |
67 … | + data.targets.set(node, nodes) | |
68 … | + data.releases.set(node, bind(document, node, data)) | |
69 … | + } else { | |
70 … | + target.appendChild(getNode(document, node)) | |
71 … | + } | |
72 … | +} | |
73 … | + | |
74 … | +function append (child) { | |
75 … | + this.appendChild(child) | |
76 … | +} | |
77 … | + | |
78 … | +function bind (document, obs, data) { | |
79 … | + return obs(function (value) { | |
80 … | + var oldNodes = data.targets.get(obs) | |
81 … | + var newNodes = getNodes(document, value) | |
82 … | + if (oldNodes) { | |
83 … | + replace(oldNodes, newNodes) | |
84 … | + data.targets.set(obs, newNodes) | |
85 … | + } | |
86 … | + }) | |
87 … | +} | |
88 … | + | |
89 … | +function replace (oldNodes, newNodes) { | |
90 … | + var parent = oldNodes[oldNodes.length - 1].parentNode | |
91 … | + var marker = oldNodes[oldNodes.length - 1].nextSibling | |
92 … | + oldNodes.filter(function (node) { | |
93 … | + return !~newNodes.indexOf(node) | |
94 … | + }).forEach(function (node) { | |
95 … | + parent.removeChild(node) | |
96 … | + }) | |
97 … | + if (marker) { | |
98 … | + newNodes.forEach(function (node) { | |
99 … | + parent.insertBefore(node, marker) | |
100 … | + }) | |
101 … | + } else { | |
102 … | + newNodes.forEach(function (node) { | |
103 … | + parent.appendChild(node) | |
104 … | + }) | |
105 … | + } | |
106 … | +} | |
107 … | + | |
108 … | +function isText (value) { | |
109 … | + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' | |
110 … | +} | |
111 … | + | |
112 … | +function getNode (document, nodeOrText) { | |
113 … | + if (nodeOrText == null) { | |
114 … | + return document.createTextNode('') | |
115 … | + } else if (isText(nodeOrText)) { | |
116 … | + return document.createTextNode(nodeOrText.toString()) | |
117 … | + } else { | |
118 … | + return nodeOrText | |
119 … | + } | |
120 … | +} | |
121 … | + | |
122 … | +function getNodes (document, nodeOrNodes) { | |
123 … | + if (Array.isArray(nodeOrNodes)) { | |
124 … | + if (nodeOrNodes.length) { | |
125 … | + var result = [] | |
126 … | + nodeOrNodes.forEach(function (item) { | |
127 … | + if (Array.isArray(item)) { | |
128 … | + getNodes(document, item).forEach(push, result) | |
129 … | + } else { | |
130 … | + result.push(getNode(document, item)) | |
131 … | + } | |
132 … | + }) | |
133 … | + return result.map(getNode.bind(this, document)) | |
134 … | + } else { | |
135 … | + return [getNode(document, null)] | |
136 … | + } | |
137 … | + } else { | |
138 … | + return [getNode(document, nodeOrNodes)] | |
139 … | + } | |
140 … | +} | |
141 … | + | |
142 … | +function push (item) { | |
143 … | + this.push(item) | |
144 … | +} | |
145 … | + | |
146 … | +function resolve (source) { | |
147 … | + return typeof source === 'function' ? source() : source | |
148 … | +} | |
149 … | + | |
150 … | +function tryInvoke (func) { | |
151 … | + if (typeof func === 'function') { | |
152 … | + func() | |
153 … | + } | |
154 … | +} |
is-observable.js | ||
---|---|---|
@@ -1,0 +1,5 @@ | ||
1 … | +module.exports = isObservable | |
2 … | + | |
3 … | +function isObservable (obj) { | |
4 … | + return typeof obj === 'function' | |
5 … | +} |
lib/apply-properties.js | ||
---|---|---|
@@ -1,0 +1,138 @@ | ||
1 … | +var isObservable = require('./is-observable') | |
2 … | +var Set = require('../set') | |
3 … | +var watch = require('../watch') | |
4 … | +var caches = new global.WeakMap() | |
5 … | + | |
6 … | +module.exports = applyProperties | |
7 … | + | |
8 … | +function applyProperties (target, properties, namespace) { | |
9 … | + var data = caches.get(target) | |
10 … | + if (!data) { | |
11 … | + data = { releases: [] } | |
12 … | + caches.set(target, data) | |
13 … | + } | |
14 … | + | |
15 … | + var classList = Set() | |
16 … | + if (target.className) { | |
17 … | + classList.add(target.className) | |
18 … | + } | |
19 … | + | |
20 … | + for (var key in properties) { | |
21 … | + var valueOrObs = properties[key] | |
22 … | + var value = resolve(valueOrObs) | |
23 … | + | |
24 … | + if (key === 'style') { | |
25 … | + // TODO: handle observable at root for style objects | |
26 … | + for (var k in value) { | |
27 … | + var styleValue = resolve(value[k]) | |
28 … | + var styleObs = isObservable(value[k]) ? value[k] : null | |
29 … | + target.style.setProperty(k, styleValue) | |
30 … | + | |
31 … | + if (styleObs) { | |
32 … | + data.releases.push(bindStyle(target, styleObs, k)) | |
33 … | + } | |
34 … | + } | |
35 … | + } else if (key === 'attributes') { | |
36 … | + for (var k in value) { | |
37 … | + var attrValue = resolve(value[k]) | |
38 … | + var attrObs = isObservable(value[k]) ? value[k] : null | |
39 … | + | |
40 … | + if (namespace) { | |
41 … | + target.setAttributeNS(namespace, k, attrValue) | |
42 … | + } else { | |
43 … | + target.setAttribute(k, attrValue) | |
44 … | + } | |
45 … | + | |
46 … | + if (attrObs) { | |
47 … | + data.releases.push(bindAttr(target, attrObs, k, namespace)) | |
48 … | + } | |
49 … | + } | |
50 … | + } else if (key === 'events') { | |
51 … | + for (var name in value) { | |
52 … | + target.addEventListener(name, value[name], true) | |
53 … | + } | |
54 … | + } else if (key.slice(0, 3) === 'ev-') { | |
55 … | + target.addEventListener(key.slice(3), value, true) | |
56 … | + } else if (key === 'className' || key === 'classList') { | |
57 … | + if (Array.isArray(value)) { | |
58 … | + value.forEach(function (v) { | |
59 … | + classList.add(v) | |
60 … | + }) | |
61 … | + } else { | |
62 … | + classList.add(valueOrObs) | |
63 … | + } | |
64 … | + } else { | |
65 … | + target[key] = value | |
66 … | + } | |
67 … | + | |
68 … | + var obs = isObservable(valueOrObs) ? valueOrObs : null | |
69 … | + if (obs) { | |
70 … | + data.releases.push(bind(target, obs, key, namespace)) | |
71 … | + } | |
72 … | + } | |
73 … | + | |
74 … | + watch(classList, function (value) { | |
75 … | + value = [].concat.apply([], value).filter(present).join(' ') | |
76 … | + if (value || target.className) { | |
77 … | + target.className = value | |
78 … | + } | |
79 … | + }) | |
80 … | +} | |
81 … | + | |
82 … | +applyProperties.destroy = function (target) { | |
83 … | + var data = caches.get(target) | |
84 … | + if (data) { | |
85 … | + while (data.releases.length) { | |
86 … | + data.releases.pop()() | |
87 … | + } | |
88 … | + caches.delete(target) | |
89 … | + } | |
90 … | +} | |
91 … | + | |
92 … | +function bindStyle (target, styleObs, key) { | |
93 … | + return styleObs(function (value) { | |
94 … | + target.style.setProperty(key, value) | |
95 … | + }) | |
96 … | +} | |
97 … | + | |
98 … | +function bindAttr (target, attrObs, key, namespace) { | |
99 … | + return attrObs(function (value) { | |
100 … | + if (value == null) { | |
101 … | + if (namespace) { | |
102 … | + target.removeAttributeNS(namespace, key) | |
103 … | + } else { | |
104 … | + target.removeAttribute(key) | |
105 … | + } | |
106 … | + } else { | |
107 … | + if (namespace) { | |
108 … | + target.setAttributeNS(namespace, key, value) | |
109 … | + } else { | |
110 … | + target.setAttribute(key, value) | |
111 … | + } | |
112 … | + } | |
113 … | + }) | |
114 … | +} | |
115 … | + | |
116 … | +function bind (target, obs, key, namespace) { | |
117 … | + return obs(function (toValue) { | |
118 … | + var fromValue = namespace | |
119 … | + ? target.getAttributeNS(namespace, key) | |
120 … | + : target.getAttribute(key) | |
121 … | + | |
122 … | + if (fromValue !== toValue) { | |
123 … | + if (namespace) { | |
124 … | + target.setAttributeNS(namespace, key, toValue) | |
125 … | + } else { | |
126 … | + target.setAttribute(key, toValue) | |
127 … | + } | |
128 … | + } | |
129 … | + }) | |
130 … | +} | |
131 … | + | |
132 … | +function present (val) { | |
133 … | + return val != null | |
134 … | +} | |
135 … | + | |
136 … | +function resolve (source) { | |
137 … | + return typeof source === 'function' ? source() : source | |
138 … | +} |
lib/parse-tag.js | ||
---|---|---|
@@ -1,0 +1,52 @@ | ||
1 … | +// FROM: https://raw.githubusercontent.com/Matt-Esch/virtual-dom/master/virtual-hyperscript/parse-tag.js | |
2 … | + | |
3 … | +'use strict' | |
4 … | + | |
5 … | +var split = require('browser-split') | |
6 … | + | |
7 … | +var classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/ | |
8 … | +var notClassId = /^\.|#/ | |
9 … | + | |
10 … | +module.exports = parseTag | |
11 … | + | |
12 … | +function parseTag (tag, attributes, namespace) { | |
13 … | + if (!tag) { | |
14 … | + return 'DIV' | |
15 … | + } | |
16 … | + | |
17 … | + var noId = !(attributes.hasOwnProperty('id')) | |
18 … | + | |
19 … | + var tagParts = split(tag, classIdSplit) | |
20 … | + var tagName = null | |
21 … | + | |
22 … | + if (notClassId.test(tagParts[1])) { | |
23 … | + tagName = 'DIV' | |
24 … | + } | |
25 … | + | |
26 … | + var classes, part, type, i, id | |
27 … | + | |
28 … | + for (i = 0; i < tagParts.length; i++) { | |
29 … | + part = tagParts[i] | |
30 … | + | |
31 … | + if (!part) { | |
32 … | + continue | |
33 … | + } | |
34 … | + | |
35 … | + type = part.charAt(0) | |
36 … | + | |
37 … | + if (!tagName) { | |
38 … | + tagName = part | |
39 … | + } else if (type === '.') { | |
40 … | + classes = classes || [] | |
41 … | + classes.push(part.substring(1, part.length)) | |
42 … | + } else if (type === '#' && noId) { | |
43 … | + id = part.substring(1, part.length) | |
44 … | + } | |
45 … | + } | |
46 … | + | |
47 … | + return { | |
48 … | + tagName: namespace ? tagName : tagName.toUpperCase(), | |
49 … | + classes: classes, | |
50 … | + id: id | |
51 … | + } | |
52 … | +} |
map.js | |||
---|---|---|---|
@@ -1,0 +1,210 @@ | |||
1 … | +var resolve = require('./resolve') | ||
2 … | +var computed = require('./computed') | ||
3 … | + | ||
4 … | +module.exports = Map | ||
5 … | + | ||
6 … | +function Map (obs, lambda) { | ||
7 … | + var releases = [] | ||
8 … | + var live = false | ||
9 … | + var lazy = false | ||
10 … | + var listeners = [] | ||
11 … | + | ||
12 … | + var lastValues = new global.Map() | ||
13 … | + var items = [] | ||
14 … | + | ||
15 … | + var raw = [] | ||
16 … | + var values = [] | ||
17 … | + var watches = [] | ||
18 … | + | ||
19 … | + var result = function MutantMap (listener) { | ||
20 … | + if (!listener) { | ||
21 … | + return getValue() | ||
22 … | + } | ||
23 … | + | ||
24 … | + if (typeof listener !== 'function') { | ||
25 … | + throw new Error('Listeners must be functions.') | ||
26 … | + } | ||
27 … | + | ||
28 … | + listeners.push(listener) | ||
29 … | + listen() | ||
30 … | + | ||
31 … | + return function remove () { | ||
32 … | + for (var i = 0, len = listeners.length; i < len; i++) { | ||
33 … | + if (listeners[i] === listener) { | ||
34 … | + listeners.splice(i, 1) | ||
35 … | + break | ||
36 … | + } | ||
37 … | + } | ||
38 … | + if (!listeners.length) { | ||
39 … | + unlisten() | ||
40 … | + } | ||
41 … | + } | ||
42 … | + } | ||
43 … | + | ||
44 … | + result.get = function (index) { | ||
45 … | + return raw[index] | ||
46 … | + } | ||
47 … | + | ||
48 … | + result.getLength = function (index) { | ||
49 … | + return raw.length | ||
50 … | + } | ||
51 … | + | ||
52 … | + result.includes = function (valueOrObs) { | ||
53 … | + return !!~raw.indexOf(valueOrObs) | ||
54 … | + } | ||
55 … | + | ||
56 … | + result.indexOf = function (valueOrObs) { | ||
57 … | + return raw.indexOf(valueOrObs) | ||
58 … | + } | ||
59 … | + | ||
60 … | + return result | ||
61 … | + | ||
62 … | + // scoped | ||
63 … | + | ||
64 … | + function listen () { | ||
65 … | + if (!live) { | ||
66 … | + live = true | ||
67 … | + lazy = true | ||
68 … | + if (typeof obs === 'function') { | ||
69 … | + releases.push(obs(onUpdate)) | ||
70 … | + } | ||
71 … | + rebindAll() | ||
72 … | + } | ||
73 … | + } | ||
74 … | + | ||
75 … | + function unlisten () { | ||
76 … | + if (live) { | ||
77 … | + live = false | ||
78 … | + while (releases.length) { | ||
79 … | + releases.pop()() | ||
80 … | + } | ||
81 … | + rebindAll() | ||
82 … | + } | ||
83 … | + } | ||
84 … | + | ||
85 … | + function onUpdate () { | ||
86 … | + if (update()) { | ||
87 … | + broadcast() | ||
88 … | + } | ||
89 … | + } | ||
90 … | + | ||
91 … | + function update () { | ||
92 … | + var changed = false | ||
93 … | + | ||
94 … | + if (items.length !== getLength(obs)) { | ||
95 … | + changed = true | ||
96 … | + } | ||
97 … | + | ||
98 … | + for (var i = 0, len = getLength(obs); i < len; i++) { | ||
99 … | + var item = get(obs, i) | ||
100 … | + | ||
101 … | + if (typeof item === 'object') { | ||
102 … | + items[i] = item | ||
103 … | + raw[i] = lambda(item) | ||
104 … | + values[i] = resolve(raw[i]) | ||
105 … | + changed = true | ||
106 … | + rebind(i) | ||
107 … | + } else if (item !== items[i]) { | ||
108 … | + items[i] = item | ||
109 … | + if (!lastValues.has(item)) { | ||
110 … | + lastValues.set(item, lambda(item)) | ||
111 … | + } | ||
112 … | + raw[i] = lastValues.get(item) | ||
113 … | + values[i] = resolve(raw[i]) | ||
114 … | + changed = true | ||
115 … | + rebind(i) | ||
116 … | + } | ||
117 … | + } | ||
118 … | + | ||
119 … | + if (changed) { | ||
120 … | + // clean up cache | ||
121 … | + Array.from(lastValues.keys()).filter(notIncluded, obs).forEach(deleteEntry, lastValues) | ||
122 … | + items.length = getLength(obs) | ||
123 … | + } | ||
124 … | + | ||
125 … | + return changed | ||
126 … | + } | ||
127 … | + | ||
128 … | + function rebind (index) { | ||
129 … | + if (watches[index]) { | ||
130 … | + watches[index]() | ||
131 … | + watches[index] = null | ||
132 … | + } | ||
133 … | + | ||
134 … | + if (live) { | ||
135 … | + if (typeof raw[index] === 'function') { | ||
136 … | + watches[index] = updateValue(raw[index], index) | ||
137 … | + } | ||
138 … | + } | ||
139 … | + } | ||
140 … | + | ||
141 … | + function rebindAll () { | ||
142 … | + for (var i = 0; i < raw.length; i++) { | ||
143 … | + rebind(i) | ||
144 … | + } | ||
145 … | + } | ||
146 … | + | ||
147 … | + function updateValue (obs, index) { | ||
148 … | + return obs(function (value) { | ||
149 … | + if (values[index] !== value || typeof value === 'object') { | ||
150 … | + values[index] = value | ||
151 … | + broadcast() | ||
152 … | + } | ||
153 … | + }) | ||
154 … | + } | ||
155 … | + | ||
156 … | + function broadcast () { | ||
157 … | + for (var i = 0, len = listeners.length; i < len; i++) { | ||
158 … | + listeners[i](values) | ||
159 … | + } | ||
160 … | + } | ||
161 … | + | ||
162 … | + function getValue () { | ||
163 … | + if (!live || lazy) { | ||
164 … | + lazy = false | ||
165 … | + update() | ||
166 … | + } | ||
167 … | + return values | ||
168 … | + } | ||
169 … | +} | ||
170 … | + | ||
171 … | +function get (target, index) { | ||
172 … | + if (typeof target === 'function' && !target.get) { | ||
173 … | + target = target() | ||
174 … | + } | ||
175 … | + | ||
176 … | + if (Array.isArray(target)) { | ||
177 … | + return target[index] | ||
178 … | + } else if (target && target.get) { | ||
179 … | + return target.get(index) | ||
180 … | + } | ||
181 … | +} | ||
182 … | + | ||
183 … | +function getLength (target) { | ||
184 … | + if (typeof target === 'function' && !target.getLength) { | ||
185 … | + target = target() | ||
186 … | + } | ||
187 … | + | ||
188 … | + if (Array.isArray(target)) { | ||
189 … | + return target.length | ||
190 … | + } else if (target && target.get) { | ||
191 … | + return target.getLength() | ||
192 … | + } | ||
193 … | + | ||
194 … | + return 0 | ||
195 … | +} | ||
196 … | + | ||
197 … | +function notIncluded (value) { | ||
198 … | + if (typeof this === 'function' && !value.includes) { | ||
199 … | + var array = this() | ||
200 … | + if (array && array.includes) { | ||
201 … | + array.includes(value) | ||
202 … | + } | ||
203 … | + } else { | ||
204 … | + return !this.includes(value) | ||
205 … | + } | ||
206 … | +} | ||
207 … | + | ||
208 … | +function deleteEntry (entry) { | ||
209 … | + this.delete(entry) | ||
210 … | +} |
package.json | ||
---|---|---|
@@ -1,0 +1,17 @@ | ||
1 … | +{ | |
2 … | + "name": "@mmckegg/mutant", | |
3 … | + "version": "0.0.0", | |
4 … | + "description": "", | |
5 … | + "main": "array.js", | |
6 … | + "directories": { | |
7 … | + "test": "test" | |
8 … | + }, | |
9 … | + "scripts": { | |
10 … | + "test": "echo \"Error: no test specified\" && exit 1" | |
11 … | + }, | |
12 … | + "author": "", | |
13 … | + "license": "ISC", | |
14 … | + "dependencies": { | |
15 … | + "browser-split": "0.0.1" | |
16 … | + } | |
17 … | +} |
resolve.js | ||
---|---|---|
@@ -1,0 +1,5 @@ | ||
1 … | +module.exports = resolve | |
2 … | + | |
3 … | +function resolve (source) { | |
4 … | + return typeof source === 'function' ? source() : source | |
5 … | +} |
send.js | ||
---|---|---|
@@ -1,0 +1,18 @@ | ||
1 … | +module.exports = Send | |
2 … | + | |
3 … | +function Send (fn, data, opts) { | |
4 … | + return { | |
5 … | + fn: fn, | |
6 … | + data: data, | |
7 … | + opts: opts, | |
8 … | + event: null, | |
9 … | + handleEvent: handleEvent | |
10 … | + } | |
11 … | +} | |
12 … | + | |
13 … | +function handleEvent (e) { | |
14 … | + e.stopPropagation() | |
15 … | + e.preventDefault() | |
16 … | + this.event = e | |
17 … | + this.fn(this.data) | |
18 … | +} |
set.js | ||
---|---|---|
@@ -1,0 +1,107 @@ | ||
1 … | +var Value = require('./value') | |
2 … | + | |
3 … | +module.exports = Set | |
4 … | + | |
5 … | +function Set (defaultValues) { | |
6 … | + var object = [] | |
7 … | + var sources = [] | |
8 … | + var releases = [] | |
9 … | + | |
10 … | + if (defaultValues && defaultValues.length) { | |
11 … | + defaultValues.forEach(function (valueOrObs) { | |
12 … | + if (!~sources.indexOf(valueOrObs)) { | |
13 … | + sources.push(valueOrObs) | |
14 … | + releases[sources.length - 1] = typeof valueOrObs === 'function' | |
15 … | + ? valueOrObs(refresh) | |
16 … | + : null | |
17 … | + } | |
18 … | + }) | |
19 … | + update() | |
20 … | + } | |
21 … | + | |
22 … | + var observable = Value(object) | |
23 … | + var broadcast = observable.set | |
24 … | + | |
25 … | + observable.add = function (valueOrObs) { | |
26 … | + if (!~sources.indexOf(valueOrObs)) { | |
27 … | + sources.push(valueOrObs) | |
28 … | + releases[sources.length - 1] = typeof valueOrObs === 'function' | |
29 … | + ? valueOrObs(refresh) | |
30 … | + : null | |
31 … | + refresh() | |
32 … | + } | |
33 … | + } | |
34 … | + | |
35 … | + observable.clear = function () { | |
36 … | + releases.forEach(tryInvoke) | |
37 … | + sources.length = 0 | |
38 … | + releases.length = 0 | |
39 … | + refresh() | |
40 … | + } | |
41 … | + | |
42 … | + observable.delete = function (valueOrObs) { | |
43 … | + var index = sources.indexOf(valueOrObs) | |
44 … | + if (~index) { | |
45 … | + sources.splice(index, 1) | |
46 … | + releases.splice(index, 1).forEach(tryInvoke) | |
47 … | + refresh() | |
48 … | + } | |
49 … | + } | |
50 … | + | |
51 … | + observable.has = function (valueOrObs) { | |
52 … | + return !!~object.indexOf(valueOrObs) | |
53 … | + } | |
54 … | + | |
55 … | + observable.set = function (values) { | |
56 … | + sources.length = 0 | |
57 … | + values.forEach(function (value) { | |
58 … | + sources.push(value) | |
59 … | + }) | |
60 … | + refresh() | |
61 … | + } | |
62 … | + | |
63 … | + observable.destroy = observable.clear | |
64 … | + | |
65 … | + return observable | |
66 … | + | |
67 … | + function refresh () { | |
68 … | + update() | |
69 … | + broadcast(object) | |
70 … | + } | |
71 … | + | |
72 … | + function update () { | |
73 … | + var currentValues = object.map(get) | |
74 … | + var newValues = sources.map(resolve) | |
75 … | + currentValues.filter(notIncluded, newValues).forEach(removeFrom, object) | |
76 … | + newValues.filter(notIncluded, currentValues).forEach(addTo, object) | |
77 … | + } | |
78 … | +} | |
79 … | + | |
80 … | +function get (value) { | |
81 … | + return value | |
82 … | +} | |
83 … | + | |
84 … | +function resolve (source) { | |
85 … | + return typeof source === 'function' ? source() : source | |
86 … | +} | |
87 … | + | |
88 … | +function notIncluded (value) { | |
89 … | + return !~this.indexOf(value) | |
90 … | +} | |
91 … | + | |
92 … | +function removeFrom (item) { | |
93 … | + var index = this.indexOf(item) | |
94 … | + if (~index) { | |
95 … | + this.splice(index, 1) | |
96 … | + } | |
97 … | +} | |
98 … | + | |
99 … | +function addTo (item) { | |
100 … | + this.push(item) | |
101 … | +} | |
102 … | + | |
103 … | +function tryInvoke (func) { | |
104 … | + if (typeof func === 'function') { | |
105 … | + func() | |
106 … | + } | |
107 … | +} |
struct.js | ||
---|---|---|
@@ -1,0 +1,70 @@ | ||
1 … | +var Value = require('./value') | |
2 … | + | |
3 … | +module.exports = Struct | |
4 … | + | |
5 … | +var blackList = { | |
6 … | + 'length': 'Clashes with `Function.prototype.length`.\n', | |
7 … | + 'name': 'Clashes with `Function.prototype.name`\n', | |
8 … | + 'destroy': '`destroy` is a reserved key of struct\n' | |
9 … | +} | |
10 … | + | |
11 … | +function Struct (properties) { | |
12 … | + var object = Object.create({}) | |
13 … | + var observable = Value(object) | |
14 … | + var broadcast = observable.set | |
15 … | + var keys = Object.keys(properties) | |
16 … | + var suspendBroadcast = false | |
17 … | + | |
18 … | + var releases = keys.forEach(function (key) { | |
19 … | + if (blackList.hasOwnProperty(key)) { | |
20 … | + throw new Error("Cannot create a struct with a key named '" + key + "'.\n" + blackList[key]) | |
21 … | + } | |
22 … | + | |
23 … | + var obs = typeof properties[key] === 'function' | |
24 … | + ? properties[key] | |
25 … | + : Value(properties[key]) | |
26 … | + | |
27 … | + object[key] = obs() | |
28 … | + observable[key] = obs | |
29 … | + | |
30 … | + return obs(function (val) { | |
31 … | + object[key] = val | |
32 … | + if (!suspendBroadcast) { | |
33 … | + broadcast(object) | |
34 … | + } | |
35 … | + }) | |
36 … | + }) | |
37 … | + | |
38 … | + observable.destroy = function () { | |
39 … | + while (releases.length) { | |
40 … | + releases.pop()() | |
41 … | + } | |
42 … | + } | |
43 … | + | |
44 … | + observable.set = function (values) { | |
45 … | + var lastValue = suspendBroadcast | |
46 … | + suspendBroadcast = true | |
47 … | + values = values || {} | |
48 … | + | |
49 … | + // update inner observables | |
50 … | + keys.forEach(function (key) { | |
51 … | + if (observable[key]() !== values[key]) { | |
52 … | + observable[key].set(values[key]) | |
53 … | + } | |
54 … | + }) | |
55 … | + | |
56 … | + // store additional keys (but don't create observables) | |
57 … | + Object.keys(values).forEach(function (key) { | |
58 … | + if (!(key in properties)) { | |
59 … | + object[key] = values[key] | |
60 … | + } | |
61 … | + }) | |
62 … | + | |
63 … | + suspendBroadcast = lastValue | |
64 … | + if (!suspendBroadcast) { | |
65 … | + broadcast(object) | |
66 … | + } | |
67 … | + } | |
68 … | + | |
69 … | + return observable | |
70 … | +} |
test/dom.js | ||
---|---|---|
@@ -1,0 +1,44 @@ | ||
1 … | +var Struct = require('../struct') | |
2 … | +var send = require('../send') | |
3 … | +var h = require('../html-element') | |
4 … | + | |
5 … | +var state = Struct({ | |
6 … | + text: 'Test', | |
7 … | + color: 'red', | |
8 … | + value: 0 | |
9 … | +}) | |
10 … | + | |
11 … | +var element = h('div.cool', { | |
12 … | + class: ['cool', state.text], | |
13 … | + style: { | |
14 … | + 'background-color': state.color | |
15 … | + } | |
16 … | +}, [ | |
17 … | + h('div', [ | |
18 … | + state.text, ' ', state.value, ' ', h('strong', 'test') | |
19 … | + ]), | |
20 … | + h('div', [ | |
21 … | + h('button', { | |
22 … | + 'ev-click': send(state.color.set, 'blue') | |
23 … | + }, 'Change color') | |
24 … | + ]) | |
25 … | +]) | |
26 … | + | |
27 … | +setTimeout(function () { | |
28 … | + state.text.set('Another value') | |
29 … | +}, 5000) | |
30 … | + | |
31 … | +setInterval(function () { | |
32 … | + state.value.set(state.value() + 1) | |
33 … | +}, 1000) | |
34 … | + | |
35 … | +setInterval(function () { | |
36 … | + // bulk update state | |
37 … | + state.set({ | |
38 … | + text: 'Retrieved from server (not really)', | |
39 … | + color: '#FFEECC', | |
40 … | + value: 1337 | |
41 … | + }) | |
42 … | +}, 10000) | |
43 … | + | |
44 … | +document.body.appendChild(element) |
test/test-array.js | ||
---|---|---|
@@ -1,0 +1,17 @@ | ||
1 … | +var Array = require('../array') | |
2 … | +var Value = require('../value') | |
3 … | + | |
4 … | +var value = Value('human') | |
5 … | +var array = Array(['cat']) | |
6 … | +array(x => console.log(x)) | |
7 … | + | |
8 … | +array.push('dog') | |
9 … | +array.push('cow') | |
10 … | +array.push(value) | |
11 … | +array.push('chicken') | |
12 … | +console.log('shift =>', array.shift()) | |
13 … | +array.push('wolf') | |
14 … | +array.insert('sheep', 0) | |
15 … | +console.log('pop =>', array.pop()) | |
16 … | + | |
17 … | +value.set('monkey') |
test/test-map.js | ||
---|---|---|
@@ -1,0 +1,26 @@ | ||
1 … | +var Array = require('../array') | |
2 … | +var Map = require('../map') | |
3 … | +var Value = require('../value') | |
4 … | +var computed = require('../computed') | |
5 … | + | |
6 … | +var value = Value('human') | |
7 … | +var array = Array(['cat']) | |
8 … | +var map = Map(array, function (obj) { | |
9 … | + if (typeof obj === 'function') { | |
10 … | + console.log('mapping => obs') | |
11 … | + return computed([obj], (x) => x + ' [dynamic]') | |
12 … | + } else { | |
13 … | + console.log('mapping => ', obj) | |
14 … | + return obj + ' [static]' | |
15 … | + } | |
16 … | +}) | |
17 … | + | |
18 … | +map(x => console.log(x)) | |
19 … | + | |
20 … | +array.push('dog') | |
21 … | +array.push('cow') | |
22 … | +array.push(value) | |
23 … | +array.push('chicken') | |
24 … | +array.push('wolf') | |
25 … | +array.insert('sheep', 0) | |
26 … | +value.set('monkey') |
test/test-set.js | ||
---|---|---|
@@ -1,0 +1,17 @@ | ||
1 … | +var Set = require('../set') | |
2 … | +var Value = require('../value') | |
3 … | + | |
4 … | +var set = Set() | |
5 … | +var value = Value('ducks') | |
6 … | +set(x => console.log(x)) | |
7 … | + | |
8 … | +set.add('cats') | |
9 … | +set.add('dogs') | |
10 … | +set.add('logs') | |
11 … | +set.add('cats') | |
12 … | +set.add(value) | |
13 … | + | |
14 … | +value.set('cows') | |
15 … | + | |
16 … | +set.delete('dogs') | |
17 … | +set.set(['dogs', 'logs', value]) |
test/test-struct.js | ||
---|---|---|
@@ -1,0 +1,28 @@ | ||
1 … | +var Struct = require('../struct') | |
2 … | + | |
3 … | +var struct = Struct({ | |
4 … | + a: 'Hello', | |
5 … | + b: 'You', | |
6 … | + c: 123, | |
7 … | + d: Struct({ | |
8 … | + tinker: 'value' | |
9 … | + }) | |
10 … | +}) | |
11 … | + | |
12 … | +console.log(struct()) | |
13 … | +struct(x => console.log(x)) | |
14 … | +struct.a(x => console.log('a =>', x)) | |
15 … | +struct.b(x => console.log('b =>', x)) | |
16 … | +struct.c(x => console.log('c =>', x)) | |
17 … | +struct.d(x => console.log('d =>', x)) | |
18 … | + | |
19 … | +struct.b.set('Cat') | |
20 … | + | |
21 … | +struct.set({ | |
22 … | + a: 'Hello', | |
23 … | + b: 'Cat', | |
24 … | + c: 123, | |
25 … | + d: { | |
26 … | + tinker: 456 | |
27 … | + } | |
28 … | +}) |
test/test-varhash.js | ||
---|---|---|
@@ -1,0 +1,15 @@ | ||
1 … | +var Map = require('../map') | |
2 … | +var Value = require('../value') | |
3 … | + | |
4 … | +var value = Value('blah') | |
5 … | +var map = Map({cat: 'meow'}) | |
6 … | +map(x => console.log(x)) | |
7 … | + | |
8 … | +map.put('dog', 'woof') | |
9 … | +map.put('sheep', 'baa') | |
10 … | +map.put('cow', 'moo') | |
11 … | +map.put('human', value) | |
12 … | +map.delete('sheep') | |
13 … | +console.log('keys =>', map.keys()) | |
14 … | +map.set({ cat: 'meow', dog: 'woof', sheep: 'baa', human: value }) | |
15 … | +map.clear() |
transforms/flatten.js |
---|
transforms/lookup.js |
---|
transforms/sort.js |
---|
transforms/uniq.js | ||
---|---|---|
@@ -1,0 +1,49 @@ | ||
1 … | +var Value = require('./value') | |
2 … | + | |
3 … | +module.exports = Uniq | |
4 … | + | |
5 … | +function Uniq (obs) { | |
6 … | + var object = [] | |
7 … | + update() | |
8 … | + | |
9 … | + var observable = Value(object) | |
10 … | + var broadcast = observable.set | |
11 … | + obs(refresh) | |
12 … | + | |
13 … | + observable.has = function (valueOrObs) { | |
14 … | + return !!~object.indexOf(valueOrObs) | |
15 … | + } | |
16 … | + | |
17 … | + return observable | |
18 … | + | |
19 … | + function update () { | |
20 … | + var currentValues = object.map(get) | |
21 … | + var newValues = obs() | |
22 … | + currentValues.filter(notIncluded, newValues).forEach(removeFrom, object) | |
23 … | + newValues.filter(notIncluded, currentValues).forEach(addTo, object) | |
24 … | + } | |
25 … | + | |
26 … | + function refresh () { | |
27 … | + update() | |
28 … | + broadcast(object) | |
29 … | + } | |
30 … | +} | |
31 … | + | |
32 … | +function get (value) { | |
33 … | + return value | |
34 … | +} | |
35 … | + | |
36 … | +function notIncluded (value) { | |
37 … | + return !~this.indexOf(value) | |
38 … | +} | |
39 … | + | |
40 … | +function removeFrom (item) { | |
41 … | + var index = this.indexOf(item) | |
42 … | + if (~index) { | |
43 … | + this.splice(index, 1) | |
44 … | + } | |
45 … | +} | |
46 … | + | |
47 … | +function addTo (item) { | |
48 … | + this.push(item) | |
49 … | +} |
value.js | ||
---|---|---|
@@ -1,0 +1,37 @@ | ||
1 … | +module.exports = Observable | |
2 … | + | |
3 … | +function Observable (value) { | |
4 … | + var listeners = [] | |
5 … | + value = value === undefined ? null : value | |
6 … | + | |
7 … | + observable.set = function (v) { | |
8 … | + value = v | |
9 … | + | |
10 … | + for (var i = 0, len = listeners.length; i < len; i++) { | |
11 … | + listeners[i](v) | |
12 … | + } | |
13 … | + } | |
14 … | + | |
15 … | + return observable | |
16 … | + | |
17 … | + function observable (listener) { | |
18 … | + if (!listener) { | |
19 … | + return value | |
20 … | + } | |
21 … | + | |
22 … | + if (typeof listener !== 'function') { | |
23 … | + throw new Error('Listeners must be functions.') | |
24 … | + } | |
25 … | + | |
26 … | + listeners.push(listener) | |
27 … | + | |
28 … | + return function remove () { | |
29 … | + for (var i = 0, len = listeners.length; i < len; i++) { | |
30 … | + if (listeners[i] === listener) { | |
31 … | + listeners.splice(i, 1) | |
32 … | + break | |
33 … | + } | |
34 … | + } | |
35 … | + } | |
36 … | + } | |
37 … | +} |
varhash.js | ||
---|---|---|
@@ -1,0 +1,100 @@ | ||
1 … | +var Value = require('./value') | |
2 … | + | |
3 … | +module.exports = Varhash | |
4 … | + | |
5 … | +function Varhash (defaultValues) { | |
6 … | + var object = Object.create({}) | |
7 … | + var sources = [] | |
8 … | + var releases = [] | |
9 … | + | |
10 … | + if (defaultValues) { | |
11 … | + Object.keys(defaultValues).forEach(function (key) { | |
12 … | + put(key, defaultValues[key]) | |
13 … | + }) | |
14 … | + } | |
15 … | + | |
16 … | + var observable = Value(object) | |
17 … | + var broadcast = observable.set | |
18 … | + | |
19 … | + observable.put = function (key, valueOrObs) { | |
20 … | + put(key, valueOrObs) | |
21 … | + broadcast(object) | |
22 … | + } | |
23 … | + | |
24 … | + observable.get = function (key) { | |
25 … | + return sources[key] | |
26 … | + } | |
27 … | + | |
28 … | + observable.keys = function () { | |
29 … | + return Object.keys(sources) | |
30 … | + } | |
31 … | + | |
32 … | + observable.clear = function () { | |
33 … | + Object.keys(sources).forEach(function (key) { | |
34 … | + tryInvoke(releases[key]) | |
35 … | + delete sources[key] | |
36 … | + delete releases[key] | |
37 … | + delete object[key] | |
38 … | + }) | |
39 … | + broadcast(object) | |
40 … | + } | |
41 … | + | |
42 … | + observable.delete = function (key) { | |
43 … | + tryInvoke(releases[key]) | |
44 … | + delete sources[key] | |
45 … | + delete releases[key] | |
46 … | + delete object[key] | |
47 … | + broadcast(object) | |
48 … | + } | |
49 … | + | |
50 … | + observable.includes = function (valueOrObs) { | |
51 … | + return !!~object.indexOf(valueOrObs) | |
52 … | + } | |
53 … | + | |
54 … | + observable.set = function (values) { | |
55 … | + Object.keys(sources).forEach(function (key) { | |
56 … | + tryInvoke(releases[key]) | |
57 … | + delete sources[key] | |
58 … | + delete releases[key] | |
59 … | + delete object[key] | |
60 … | + }) | |
61 … | + | |
62 … | + Object.keys(values).forEach(function (key) { | |
63 … | + put(key, values[key]) | |
64 … | + }) | |
65 … | + | |
66 … | + broadcast(object) | |
67 … | + } | |
68 … | + | |
69 … | + observable.destroy = observable.clear | |
70 … | + | |
71 … | + return observable | |
72 … | + | |
73 … | + // scoped | |
74 … | + | |
75 … | + function put (key, valueOrObs) { | |
76 … | + tryInvoke(releases[key]) | |
77 … | + sources[key] = valueOrObs | |
78 … | + releases[key] = bind(key, valueOrObs) | |
79 … | + object[key] = resolve(valueOrObs) | |
80 … | + } | |
81 … | + | |
82 … | + function bind (key, valueOrObs) { | |
83 … | + return typeof valueOrObs === 'function' ? valueOrObs(update.bind(this, key)) : null | |
84 … | + } | |
85 … | + | |
86 … | + function update (key, value) { | |
87 … | + object[key] = value | |
88 … | + broadcast(object) | |
89 … | + } | |
90 … | +} | |
91 … | + | |
92 … | +function resolve (source) { | |
93 … | + return typeof source === 'function' ? source() : source | |
94 … | +} | |
95 … | + | |
96 … | +function tryInvoke (func) { | |
97 … | + if (typeof func === 'function') { | |
98 … | + func() | |
99 … | + } | |
100 … | +} |
watch.js | ||
---|---|---|
@@ -1,0 +1,14 @@ | ||
1 … | +module.exports = watch | |
2 … | + | |
3 … | +function watch (observable, listener) { | |
4 … | + if (typeof observable === 'function') { | |
5 … | + var remove = observable(listener) | |
6 … | + listener(observable()) | |
7 … | + return remove | |
8 … | + } else { | |
9 … | + listener(observable) | |
10 … | + return noop | |
11 … | + } | |
12 … | +} | |
13 … | + | |
14 … | +function noop () {} |
Built with git-ssb-web