deku 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE +25 -0
  7. data/README.md +28 -0
  8. data/Rakefile +1 -0
  9. data/deku.gemspec +23 -0
  10. data/deps/node_modules/deku.js +2 -0
  11. data/deps/node_modules/deku/.editorconfig +16 -0
  12. data/deps/node_modules/deku/.zuul.yml +15 -0
  13. data/deps/node_modules/deku/History.md +290 -0
  14. data/deps/node_modules/deku/LICENSE.md +7 -0
  15. data/deps/node_modules/deku/Makefile +91 -0
  16. data/deps/node_modules/deku/README.md +293 -0
  17. data/deps/node_modules/deku/index.js +4072 -0
  18. data/deps/node_modules/deku/lib/application.js +85 -0
  19. data/deps/node_modules/deku/lib/index.js +28 -0
  20. data/deps/node_modules/deku/lib/render.js +1300 -0
  21. data/deps/node_modules/deku/lib/stringify.js +105 -0
  22. data/deps/node_modules/deku/lib/svg.js +107 -0
  23. data/deps/node_modules/deku/lib/utils.js +18 -0
  24. data/deps/node_modules/deku/lib/virtual.js +247 -0
  25. data/deps/node_modules/deku/node_modules/array-flatten/LICENSE +21 -0
  26. data/deps/node_modules/deku/node_modules/array-flatten/README.md +43 -0
  27. data/deps/node_modules/deku/node_modules/array-flatten/array-flatten.js +57 -0
  28. data/deps/node_modules/deku/node_modules/array-flatten/package.json +62 -0
  29. data/deps/node_modules/deku/node_modules/component-emitter/History.md +63 -0
  30. data/deps/node_modules/deku/node_modules/component-emitter/LICENSE +24 -0
  31. data/deps/node_modules/deku/node_modules/component-emitter/Readme.md +74 -0
  32. data/deps/node_modules/deku/node_modules/component-emitter/index.js +161 -0
  33. data/deps/node_modules/deku/node_modules/component-emitter/package.json +174 -0
  34. data/deps/node_modules/deku/node_modules/component-raf/.npmignore +2 -0
  35. data/deps/node_modules/deku/node_modules/component-raf/History.md +26 -0
  36. data/deps/node_modules/deku/node_modules/component-raf/Makefile +11 -0
  37. data/deps/node_modules/deku/node_modules/component-raf/Readme.md +46 -0
  38. data/deps/node_modules/deku/node_modules/component-raf/component.json +16 -0
  39. data/deps/node_modules/deku/node_modules/component-raf/example.html +43 -0
  40. data/deps/node_modules/deku/node_modules/component-raf/index.js +34 -0
  41. data/deps/node_modules/deku/node_modules/component-raf/package.json +164 -0
  42. data/deps/node_modules/deku/node_modules/component-type/.npmignore +3 -0
  43. data/deps/node_modules/deku/node_modules/component-type/Makefile +14 -0
  44. data/deps/node_modules/deku/node_modules/component-type/Readme.md +37 -0
  45. data/deps/node_modules/deku/node_modules/component-type/component.json +13 -0
  46. data/deps/node_modules/deku/node_modules/component-type/index.js +34 -0
  47. data/deps/node_modules/deku/node_modules/component-type/package.json +120 -0
  48. data/deps/node_modules/deku/node_modules/component-type/test/index.html +17 -0
  49. data/deps/node_modules/deku/node_modules/component-type/test/mocha.css +231 -0
  50. data/deps/node_modules/deku/node_modules/component-type/test/mocha.js +5340 -0
  51. data/deps/node_modules/deku/node_modules/component-type/test/tests.js +72 -0
  52. data/deps/node_modules/deku/node_modules/dom-pool/.npmignore +1 -0
  53. data/deps/node_modules/deku/node_modules/dom-pool/Pool.js +52 -0
  54. data/deps/node_modules/deku/node_modules/dom-pool/README.md +42 -0
  55. data/deps/node_modules/deku/node_modules/dom-pool/authors.txt +4 -0
  56. data/deps/node_modules/deku/node_modules/dom-pool/bower.json +26 -0
  57. data/deps/node_modules/deku/node_modules/dom-pool/package.json +46 -0
  58. data/deps/node_modules/deku/node_modules/dom-pool/tests.html +16 -0
  59. data/deps/node_modules/deku/node_modules/dom-pool/tests.js +102 -0
  60. data/deps/node_modules/deku/node_modules/dom-walk/.npmignore +3 -0
  61. data/deps/node_modules/deku/node_modules/dom-walk/LICENCE +19 -0
  62. data/deps/node_modules/deku/node_modules/dom-walk/Makefile +2 -0
  63. data/deps/node_modules/deku/node_modules/dom-walk/README.md +23 -0
  64. data/deps/node_modules/deku/node_modules/dom-walk/example/index.js +5 -0
  65. data/deps/node_modules/deku/node_modules/dom-walk/example/static/bundle.js +211 -0
  66. data/deps/node_modules/deku/node_modules/dom-walk/example/static/index.html +16 -0
  67. data/deps/node_modules/deku/node_modules/dom-walk/index.js +24 -0
  68. data/deps/node_modules/deku/node_modules/dom-walk/package.json +57 -0
  69. data/deps/node_modules/deku/node_modules/fast.js/.jshintignore +7 -0
  70. data/deps/node_modules/deku/node_modules/fast.js/.jshintrc +80 -0
  71. data/deps/node_modules/deku/node_modules/fast.js/.npmignore +6 -0
  72. data/deps/node_modules/deku/node_modules/fast.js/.travis.yml +3 -0
  73. data/deps/node_modules/deku/node_modules/fast.js/LICENSE.md +21 -0
  74. data/deps/node_modules/deku/node_modules/fast.js/README.md +552 -0
  75. data/deps/node_modules/deku/node_modules/fast.js/array/clone.js +21 -0
  76. data/deps/node_modules/deku/node_modules/fast.js/array/concat.js +32 -0
  77. data/deps/node_modules/deku/node_modules/fast.js/array/every.js +25 -0
  78. data/deps/node_modules/deku/node_modules/fast.js/array/fill.js +29 -0
  79. data/deps/node_modules/deku/node_modules/fast.js/array/filter.js +26 -0
  80. data/deps/node_modules/deku/node_modules/fast.js/array/forEach.js +21 -0
  81. data/deps/node_modules/deku/node_modules/fast.js/array/index.js +15 -0
  82. data/deps/node_modules/deku/node_modules/fast.js/array/indexOf.js +33 -0
  83. data/deps/node_modules/deku/node_modules/fast.js/array/lastIndexOf.js +29 -0
  84. data/deps/node_modules/deku/node_modules/fast.js/array/map.js +24 -0
  85. data/deps/node_modules/deku/node_modules/fast.js/array/pluck.js +24 -0
  86. data/deps/node_modules/deku/node_modules/fast.js/array/reduce.js +35 -0
  87. data/deps/node_modules/deku/node_modules/fast.js/array/reduceRight.js +35 -0
  88. data/deps/node_modules/deku/node_modules/fast.js/array/some.js +25 -0
  89. data/deps/node_modules/deku/node_modules/fast.js/bower.json +28 -0
  90. data/deps/node_modules/deku/node_modules/fast.js/clone.js +27 -0
  91. data/deps/node_modules/deku/node_modules/fast.js/dist/bench.html +15 -0
  92. data/deps/node_modules/deku/node_modules/fast.js/dist/bench.js +19900 -0
  93. data/deps/node_modules/deku/node_modules/fast.js/dist/fast.js +1450 -0
  94. data/deps/node_modules/deku/node_modules/fast.js/dist/fast.min.js +1 -0
  95. data/deps/node_modules/deku/node_modules/fast.js/filter.js +23 -0
  96. data/deps/node_modules/deku/node_modules/fast.js/forEach.js +22 -0
  97. data/deps/node_modules/deku/node_modules/fast.js/function/apply.js +19 -0
  98. data/deps/node_modules/deku/node_modules/fast.js/function/applyNoContext.js +29 -0
  99. data/deps/node_modules/deku/node_modules/fast.js/function/applyWithContext.js +29 -0
  100. data/deps/node_modules/deku/node_modules/fast.js/function/bind.js +71 -0
  101. data/deps/node_modules/deku/node_modules/fast.js/function/bindInternal3.js +11 -0
  102. data/deps/node_modules/deku/node_modules/fast.js/function/bindInternal4.js +11 -0
  103. data/deps/node_modules/deku/node_modules/fast.js/function/index.js +7 -0
  104. data/deps/node_modules/deku/node_modules/fast.js/function/partial.js +42 -0
  105. data/deps/node_modules/deku/node_modules/fast.js/function/partialConstructor.js +45 -0
  106. data/deps/node_modules/deku/node_modules/fast.js/function/try.js +35 -0
  107. data/deps/node_modules/deku/node_modules/fast.js/index.js +241 -0
  108. data/deps/node_modules/deku/node_modules/fast.js/map.js +23 -0
  109. data/deps/node_modules/deku/node_modules/fast.js/object/assign.js +34 -0
  110. data/deps/node_modules/deku/node_modules/fast.js/object/clone.js +25 -0
  111. data/deps/node_modules/deku/node_modules/fast.js/object/filter.js +28 -0
  112. data/deps/node_modules/deku/node_modules/fast.js/object/forEach.js +23 -0
  113. data/deps/node_modules/deku/node_modules/fast.js/object/index.js +11 -0
  114. data/deps/node_modules/deku/node_modules/fast.js/object/keys.js +17 -0
  115. data/deps/node_modules/deku/node_modules/fast.js/object/map.js +26 -0
  116. data/deps/node_modules/deku/node_modules/fast.js/object/reduce.js +37 -0
  117. data/deps/node_modules/deku/node_modules/fast.js/object/reduceRight.js +37 -0
  118. data/deps/node_modules/deku/node_modules/fast.js/object/values.js +20 -0
  119. data/deps/node_modules/deku/node_modules/fast.js/package.json +73 -0
  120. data/deps/node_modules/deku/node_modules/fast.js/reduce.js +24 -0
  121. data/deps/node_modules/deku/node_modules/fast.js/reduceRight.js +24 -0
  122. data/deps/node_modules/deku/node_modules/fast.js/string/index.js +3 -0
  123. data/deps/node_modules/deku/node_modules/fast.js/string/intern.js +56 -0
  124. data/deps/node_modules/deku/node_modules/get-uid/README.md +44 -0
  125. data/deps/node_modules/deku/node_modules/get-uid/index.js +6 -0
  126. data/deps/node_modules/deku/node_modules/get-uid/package.json +56 -0
  127. data/deps/node_modules/deku/node_modules/is-dom/HISTORY.md +2 -0
  128. data/deps/node_modules/deku/node_modules/is-dom/LICENSE +21 -0
  129. data/deps/node_modules/deku/node_modules/is-dom/README.md +32 -0
  130. data/deps/node_modules/deku/node_modules/is-dom/index.js +15 -0
  131. data/deps/node_modules/deku/node_modules/is-dom/package.json +62 -0
  132. data/deps/node_modules/deku/node_modules/object-path/.npmignore +7 -0
  133. data/deps/node_modules/deku/node_modules/object-path/.travis.yml +6 -0
  134. data/deps/node_modules/deku/node_modules/object-path/LICENSE +21 -0
  135. data/deps/node_modules/deku/node_modules/object-path/README.md +96 -0
  136. data/deps/node_modules/deku/node_modules/object-path/bower.json +17 -0
  137. data/deps/node_modules/deku/node_modules/object-path/component.json +22 -0
  138. data/deps/node_modules/deku/node_modules/object-path/index.js +269 -0
  139. data/deps/node_modules/deku/node_modules/object-path/package.json +89 -0
  140. data/deps/node_modules/deku/node_modules/object-path/test.js +510 -0
  141. data/deps/node_modules/deku/node_modules/per-frame/.npmignore +68 -0
  142. data/deps/node_modules/deku/node_modules/per-frame/History.md +32 -0
  143. data/deps/node_modules/deku/node_modules/per-frame/README.md +44 -0
  144. data/deps/node_modules/deku/node_modules/per-frame/component.json +13 -0
  145. data/deps/node_modules/deku/node_modules/per-frame/index.js +37 -0
  146. data/deps/node_modules/deku/node_modules/per-frame/package.json +143 -0
  147. data/deps/node_modules/deku/node_modules/per-frame/test/test.js +94 -0
  148. data/deps/node_modules/deku/node_modules/sliced/.npmignore +2 -0
  149. data/deps/node_modules/deku/node_modules/sliced/.travis.yml +4 -0
  150. data/deps/node_modules/deku/node_modules/sliced/History.md +30 -0
  151. data/deps/node_modules/deku/node_modules/sliced/LICENSE +22 -0
  152. data/deps/node_modules/deku/node_modules/sliced/Makefile +5 -0
  153. data/deps/node_modules/deku/node_modules/sliced/README.md +62 -0
  154. data/deps/node_modules/deku/node_modules/sliced/bench.js +95 -0
  155. data/deps/node_modules/deku/node_modules/sliced/component.json +14 -0
  156. data/deps/node_modules/deku/node_modules/sliced/index.js +1 -0
  157. data/deps/node_modules/deku/node_modules/sliced/lib/sliced.js +33 -0
  158. data/deps/node_modules/deku/node_modules/sliced/package.json +52 -0
  159. data/deps/node_modules/deku/node_modules/sliced/test/index.js +80 -0
  160. data/deps/node_modules/deku/package.json +67 -0
  161. data/lib/deku.rb +11 -0
  162. data/lib/deku/application.rb +16 -0
  163. data/lib/deku/component.rb +36 -0
  164. data/lib/deku/context.rb +38 -0
  165. data/lib/deku/element_node.rb +17 -0
  166. data/lib/deku/version.rb +4 -0
  167. metadata +278 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Module dependencies.
3
+ */
4
+
5
+ var Emitter = require('component-emitter')
6
+
7
+ /**
8
+ * Expose `scene`.
9
+ */
10
+
11
+ module.exports = Application
12
+
13
+ /**
14
+ * Create a new `Application`.
15
+ *
16
+ * @param {Object} element Optional initial element
17
+ */
18
+
19
+ function Application (element) {
20
+ if (!(this instanceof Application)) return new Application(element)
21
+ this.options = {}
22
+ this.sources = {}
23
+ this.element = element
24
+ }
25
+
26
+ /**
27
+ * Mixin `Emitter`.
28
+ */
29
+
30
+ Emitter(Application.prototype)
31
+
32
+ /**
33
+ * Add a plugin
34
+ *
35
+ * @param {Function} plugin
36
+ */
37
+
38
+ Application.prototype.use = function (plugin) {
39
+ plugin(this)
40
+ return this
41
+ }
42
+
43
+ /**
44
+ * Set an option
45
+ *
46
+ * @param {String} name
47
+ */
48
+
49
+ Application.prototype.option = function (name, val) {
50
+ this.options[name] = val
51
+ return this
52
+ }
53
+
54
+ /**
55
+ * Set value used somewhere in the IO network.
56
+ */
57
+
58
+ Application.prototype.set = function (name, data) {
59
+ this.sources[name] = data
60
+ this.emit('source', name, data)
61
+ return this
62
+ }
63
+
64
+ /**
65
+ * Mount a virtual element.
66
+ *
67
+ * @param {VirtualElement} element
68
+ */
69
+
70
+ Application.prototype.mount = function (element) {
71
+ this.element = element
72
+ this.emit('mount', element)
73
+ return this
74
+ }
75
+
76
+ /**
77
+ * Remove the world. Unmount everything.
78
+ */
79
+
80
+ Application.prototype.unmount = function () {
81
+ if (!this.element) return
82
+ this.element = null
83
+ this.emit('unmount')
84
+ return this
85
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Create the application.
3
+ */
4
+
5
+ exports.tree =
6
+ exports.scene =
7
+ exports.deku = require('./application')
8
+
9
+ /**
10
+ * Render scenes to the DOM.
11
+ */
12
+
13
+ if (typeof document !== 'undefined') {
14
+ exports.render = require('./render')
15
+ }
16
+
17
+ /**
18
+ * Render scenes to a string
19
+ */
20
+
21
+ exports.renderString = require('./stringify')
22
+
23
+ /**
24
+ * Create virtual elements.
25
+ */
26
+
27
+ exports.element =
28
+ exports.dom = require('./virtual')
@@ -0,0 +1,1300 @@
1
+ /**
2
+ * Dependencies.
3
+ */
4
+
5
+ var raf = require('component-raf')
6
+ var Pool = require('dom-pool')
7
+ var walk = require('dom-walk')
8
+ var isDom = require('is-dom')
9
+ var uid = require('get-uid')
10
+ var throttle = require('per-frame')
11
+ var keypath = require('object-path')
12
+ var type = require('component-type')
13
+ var fast = require('fast.js')
14
+ var utils = require('./utils')
15
+ var svg = require('./svg')
16
+ var defaults = utils.defaults
17
+ var forEach = fast.forEach
18
+ var assign = fast.assign
19
+ var reduce = fast.reduce
20
+
21
+ /**
22
+ * All of the events can bind to
23
+ */
24
+
25
+ var events = {
26
+ onBlur: 'blur',
27
+ onChange: 'change',
28
+ onClick: 'click',
29
+ onContextMenu: 'contextmenu',
30
+ onCopy: 'copy',
31
+ onCut: 'cut',
32
+ onDoubleClick: 'dblclick',
33
+ onDrag: 'drag',
34
+ onDragEnd: 'dragend',
35
+ onDragEnter: 'dragenter',
36
+ onDragExit: 'dragexit',
37
+ onDragLeave: 'dragleave',
38
+ onDragOver: 'dragover',
39
+ onDragStart: 'dragstart',
40
+ onDrop: 'drop',
41
+ onFocus: 'focus',
42
+ onInput: 'input',
43
+ onKeyDown: 'keydown',
44
+ onKeyUp: 'keyup',
45
+ onMouseDown: 'mousedown',
46
+ onMouseMove: 'mousemove',
47
+ onMouseOut: 'mouseout',
48
+ onMouseOver: 'mouseover',
49
+ onMouseUp: 'mouseup',
50
+ onPaste: 'paste',
51
+ onScroll: 'scroll',
52
+ onSubmit: 'submit',
53
+ onTouchCancel: 'touchcancel',
54
+ onTouchEnd: 'touchend',
55
+ onTouchMove: 'touchmove',
56
+ onTouchStart: 'touchstart'
57
+ }
58
+
59
+ /**
60
+ * These elements won't be pooled
61
+ */
62
+
63
+ var avoidPooling = ['input', 'textarea'];
64
+
65
+ /**
66
+ * Expose `dom`.
67
+ */
68
+
69
+ module.exports = render
70
+
71
+ /**
72
+ * Render an app to the DOM
73
+ *
74
+ * @param {Application} app
75
+ * @param {HTMLElement} container
76
+ * @param {Object} opts
77
+ *
78
+ * @return {Object}
79
+ */
80
+
81
+ function render (app, container, opts) {
82
+ var frameId
83
+ var isRendering
84
+ var rootId = 'root'
85
+ var currentElement
86
+ var currentNativeElement
87
+ var connections = {}
88
+ var entities = {}
89
+ var pools = {}
90
+ var handlers = {}
91
+ var children = {}
92
+ children[rootId] = {}
93
+
94
+ if (!isDom(container)) {
95
+ throw new Error('Container element must be a DOM element')
96
+ }
97
+
98
+ /**
99
+ * Rendering options. Batching is only ever really disabled
100
+ * when running tests, and pooling can be disabled if the user
101
+ * is doing something stupid with the DOM in their components.
102
+ */
103
+
104
+ var options = defaults(assign({}, app.options || {}, opts || {}), {
105
+ pooling: true,
106
+ batching: true,
107
+ validateProps: false
108
+ })
109
+
110
+ /**
111
+ * Listen to DOM events
112
+ */
113
+
114
+ addNativeEventListeners()
115
+
116
+ /**
117
+ * Watch for changes to the app so that we can update
118
+ * the DOM as needed.
119
+ */
120
+
121
+ app.on('unmount', onunmount)
122
+ app.on('mount', onmount)
123
+ app.on('source', onupdate)
124
+
125
+ /**
126
+ * If the app has already mounted an element, we can just
127
+ * render that straight away.
128
+ */
129
+
130
+ if (app.element) render()
131
+
132
+ /**
133
+ * Teardown the DOM rendering so that it stops
134
+ * rendering and everything can be garbage collected.
135
+ */
136
+
137
+ function teardown () {
138
+ removeNativeEventListeners()
139
+ removeNativeElement()
140
+ app.off('unmount', onunmount)
141
+ app.off('mount', onmount)
142
+ app.off('source', onupdate)
143
+ }
144
+
145
+ /**
146
+ * Swap the current rendered node with a new one that is rendered
147
+ * from the new virtual element mounted on the app.
148
+ *
149
+ * @param {VirtualElement} element
150
+ */
151
+
152
+ function onmount () {
153
+ invalidate()
154
+ }
155
+
156
+ /**
157
+ * If the app unmounts an element, we should clear out the current
158
+ * rendered element. This will remove all the entities.
159
+ */
160
+
161
+ function onunmount () {
162
+ removeNativeElement()
163
+ currentElement = null
164
+ }
165
+
166
+ /**
167
+ * Update all components that are bound to the source
168
+ *
169
+ * @param {String} name
170
+ * @param {*} data
171
+ */
172
+
173
+ function onupdate (name, data) {
174
+ connections[name](data)
175
+ }
176
+
177
+ /**
178
+ * Render and mount a component to the native dom.
179
+ *
180
+ * @param {Entity} entity
181
+ * @return {HTMLElement}
182
+ */
183
+
184
+ function mountEntity (entity) {
185
+ register(entity)
186
+ setSources(entity)
187
+ children[entity.id] = {}
188
+ entities[entity.id] = entity
189
+
190
+ // commit initial state and props.
191
+ commit(entity)
192
+
193
+ // callback before mounting.
194
+ trigger('beforeMount', entity, [entity.context])
195
+ trigger('beforeRender', entity, [entity.context])
196
+
197
+ // render virtual element.
198
+ var virtualElement = renderEntity(entity)
199
+ // create native element.
200
+ var nativeElement = toNative(entity.id, '0', virtualElement)
201
+
202
+ entity.virtualElement = virtualElement
203
+ entity.nativeElement = nativeElement
204
+
205
+ // callback after mounting.
206
+ trigger('afterRender', entity, [entity.context, nativeElement])
207
+ trigger('afterMount', entity, [entity.context, nativeElement, setState(entity)])
208
+
209
+ return nativeElement
210
+ }
211
+
212
+ /**
213
+ * Remove a component from the native dom.
214
+ *
215
+ * @param {Entity} entity
216
+ */
217
+
218
+ function unmountEntity (entityId) {
219
+ var entity = entities[entityId]
220
+ if (!entity) return
221
+ trigger('beforeUnmount', entity, [entity.context, entity.nativeElement])
222
+ unmountChildren(entityId)
223
+ removeAllEvents(entityId)
224
+ delete entities[entityId]
225
+ delete children[entityId]
226
+ }
227
+
228
+ /**
229
+ * Render the entity and make sure it returns a node
230
+ *
231
+ * @param {Entity} entity
232
+ *
233
+ * @return {VirtualTree}
234
+ */
235
+
236
+ function renderEntity (entity) {
237
+ var component = entity.component
238
+ if (!component.render) throw new Error('Component needs a render function')
239
+ var result = component.render(entity.context, setState(entity))
240
+ if (!result) throw new Error('Render function must return an element.')
241
+ return result
242
+ }
243
+
244
+ /**
245
+ * Whenever setState or setProps is called, we mark the entity
246
+ * as dirty in the renderer. This lets us optimize the re-rendering
247
+ * and skip components that definitely haven't changed.
248
+ *
249
+ * @param {Entity} entity
250
+ *
251
+ * @return {Function} A curried function for updating the state of an entity
252
+ */
253
+
254
+ function setState (entity) {
255
+ return function (nextState) {
256
+ updateEntityState(entity, nextState)
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Tell the app it's dirty and needs to re-render. If batching is disabled
262
+ * we can just trigger a render immediately, otherwise we'll wait until
263
+ * the next available frame.
264
+ */
265
+
266
+ function invalidate () {
267
+ if (!options.batching) {
268
+ if (!isRendering) render()
269
+ } else {
270
+ if (!frameId) frameId = raf(render)
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Update the DOM. If the update fails we stop the loop
276
+ * so we don't get errors on every frame.
277
+ *
278
+ * @api public
279
+ */
280
+
281
+ function render () {
282
+ // If this is called synchronously we need to
283
+ // cancel any pending future updates
284
+ clearFrame()
285
+
286
+ // If the rendering from the previous frame is still going,
287
+ // we'll just wait until the next frame. Ideally renders should
288
+ // not take over 16ms to stay within a single frame, but this should
289
+ // catch it if it does.
290
+ if (isRendering) {
291
+ frameId = raf(render)
292
+ return
293
+ } else {
294
+ isRendering = true
295
+ }
296
+
297
+ // 1. If there isn't a native element rendered for the current mounted element
298
+ // then we need to create it from scratch.
299
+ // 2. If a new element has been mounted, we should diff them.
300
+ // 3. We should update check all child components for changes.
301
+ if (!currentNativeElement) {
302
+ currentElement = app.element
303
+ currentNativeElement = toNative(rootId, '0', currentElement)
304
+ container.appendChild(currentNativeElement)
305
+ } else if (currentElement !== app.element) {
306
+ currentNativeElement = patch(rootId, currentElement, app.element, currentNativeElement)
307
+ currentElement = app.element
308
+ updateChildren(rootId)
309
+ } else {
310
+ updateChildren(rootId)
311
+ }
312
+
313
+ // Allow rendering again.
314
+ isRendering = false
315
+ }
316
+
317
+ /**
318
+ * Clear the current scheduled frame
319
+ */
320
+
321
+ function clearFrame () {
322
+ if (!frameId) return
323
+ raf.cancel(frameId)
324
+ frameId = 0
325
+ }
326
+
327
+ /**
328
+ * Update a component.
329
+ *
330
+ * The entity is just the data object for a component instance.
331
+ *
332
+ * @param {String} id Component instance id.
333
+ */
334
+
335
+ function updateEntity (entityId) {
336
+ var entity = entities[entityId]
337
+ setSources(entity)
338
+
339
+ if (!shouldUpdate(entity)) return updateChildren(entityId)
340
+
341
+ var currentTree = entity.virtualElement
342
+ var nextProps = entity.pendingProps
343
+ var nextState = entity.pendingState
344
+ var previousState = entity.context.state
345
+ var previousProps = entity.context.props
346
+
347
+ // hook before rendering. could modify state just before the render occurs.
348
+ trigger('beforeUpdate', entity, [entity.context, nextProps, nextState])
349
+ trigger('beforeRender', entity, [entity.context])
350
+
351
+ // commit state and props.
352
+ commit(entity)
353
+
354
+ // re-render.
355
+ var nextTree = renderEntity(entity)
356
+
357
+ // if the tree is the same we can just skip this component
358
+ // but we should still check the children to see if they're dirty.
359
+ // This allows us to memoize the render function of components.
360
+ if (nextTree === currentTree) return updateChildren(entityId)
361
+
362
+ // apply new virtual tree to native dom.
363
+ entity.nativeElement = patch(entityId, currentTree, nextTree, entity.nativeElement)
364
+ entity.virtualElement = nextTree
365
+ updateChildren(entityId)
366
+
367
+ // trigger render hook
368
+ trigger('afterRender', entity, [entity.context, entity.nativeElement])
369
+
370
+ // trigger afterUpdate after all children have updated.
371
+ trigger('afterUpdate', entity, [entity.context, previousProps, previousState, setState(entity)])
372
+ }
373
+
374
+ /**
375
+ * Update all the children of an entity.
376
+ *
377
+ * @param {String} id Component instance id.
378
+ */
379
+
380
+ function updateChildren (entityId) {
381
+ forEach(children[entityId], function (childId) {
382
+ updateEntity(childId)
383
+ })
384
+ }
385
+
386
+ /**
387
+ * Remove all of the child entities of an entity
388
+ *
389
+ * @param {Entity} entity
390
+ */
391
+
392
+ function unmountChildren (entityId) {
393
+ forEach(children[entityId], function (childId) {
394
+ unmountEntity(childId)
395
+ })
396
+ }
397
+
398
+ /**
399
+ * Remove the root element. If this is called synchronously we need to
400
+ * cancel any pending future updates.
401
+ */
402
+
403
+ function removeNativeElement () {
404
+ clearFrame()
405
+ removeElement(rootId, '0', currentNativeElement)
406
+ currentNativeElement = null
407
+ }
408
+
409
+ /**
410
+ * Create a native element from a virtual element.
411
+ *
412
+ * @param {String} entityId
413
+ * @param {String} path
414
+ * @param {Object} vnode
415
+ *
416
+ * @return {HTMLDocumentFragment}
417
+ */
418
+
419
+ function toNative (entityId, path, vnode) {
420
+ switch (vnode.type) {
421
+ case 'text': return toNativeText(vnode)
422
+ case 'element': return toNativeElement(entityId, path, vnode)
423
+ case 'component': return toNativeComponent(entityId, path, vnode)
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Create a native text element from a virtual element.
429
+ *
430
+ * @param {Object} vnode
431
+ */
432
+
433
+ function toNativeText (vnode) {
434
+ return document.createTextNode(vnode.data)
435
+ }
436
+
437
+ /**
438
+ * Create a native element from a virtual element.
439
+ */
440
+
441
+ function toNativeElement (entityId, path, vnode) {
442
+ var attributes = vnode.attributes
443
+ var children = vnode.children
444
+ var tagName = vnode.tagName
445
+ var el
446
+
447
+ // create element either from pool or fresh.
448
+ if (!options.pooling || !canPool(tagName)) {
449
+ if (svg.isElement(tagName)) {
450
+ el = document.createElementNS(svg.namespace, tagName)
451
+ } else {
452
+ el = document.createElement(tagName)
453
+ }
454
+ } else {
455
+ var pool = getPool(tagName)
456
+ el = cleanup(pool.pop())
457
+ if (el.parentNode) el.parentNode.removeChild(el)
458
+ }
459
+
460
+ // set attributes.
461
+ forEach(attributes, function (value, name) {
462
+ setAttribute(entityId, path, el, name, value)
463
+ })
464
+
465
+ // store keys on the native element for fast event handling.
466
+ el.__entity__ = entityId
467
+ el.__path__ = path
468
+
469
+ // add children.
470
+ forEach(children, function (child, i) {
471
+ var childEl = toNative(entityId, path + '.' + i, child)
472
+ if (!childEl.parentNode) el.appendChild(childEl)
473
+ })
474
+
475
+ return el
476
+ }
477
+
478
+ /**
479
+ * Create a native element from a component.
480
+ */
481
+
482
+ function toNativeComponent (entityId, path, vnode) {
483
+ var child = new Entity(vnode.component, vnode.props)
484
+ children[entityId][path] = child.id
485
+ return mountEntity(child)
486
+ }
487
+
488
+ /**
489
+ * Patch an element with the diff from two trees.
490
+ */
491
+
492
+ function patch (entityId, prev, next, el) {
493
+ return diffNode('0', entityId, prev, next, el)
494
+ }
495
+
496
+ /**
497
+ * Create a diff between two tress of nodes.
498
+ */
499
+
500
+ function diffNode (path, entityId, prev, next, el) {
501
+ // Type changed. This could be from element->text, text->ComponentA,
502
+ // ComponentA->ComponentB etc. But NOT div->span. These are the same type
503
+ // (ElementNode) but different tag name.
504
+ if (prev.type !== next.type) return replaceElement(entityId, path, el, next)
505
+
506
+ switch (next.type) {
507
+ case 'text': return diffText(prev, next, el)
508
+ case 'element': return diffElement(path, entityId, prev, next, el)
509
+ case 'component': return diffComponent(path, entityId, prev, next, el)
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Diff two text nodes and update the element.
515
+ */
516
+
517
+ function diffText (previous, current, el) {
518
+ if (current.data !== previous.data) el.data = current.data
519
+ return el
520
+ }
521
+
522
+ /**
523
+ * Diff the children of an ElementNode.
524
+ */
525
+
526
+ function diffChildren (path, entityId, prev, next, el) {
527
+ var positions = []
528
+ var hasKeys = false
529
+ var childNodes = Array.prototype.slice.apply(el.childNodes)
530
+ var leftKeys = reduce(prev.children, keyMapReducer, {})
531
+ var rightKeys = reduce(next.children, keyMapReducer, {})
532
+ var currentChildren = assign({}, children[entityId])
533
+
534
+ function keyMapReducer (acc, child) {
535
+ if (child.key != null) {
536
+ acc[child.key] = child
537
+ hasKeys = true
538
+ }
539
+ return acc
540
+ }
541
+
542
+ // Diff all of the nodes that have keys. This lets us re-used elements
543
+ // instead of overriding them and lets us move them around.
544
+ if (hasKeys) {
545
+
546
+ // Removals
547
+ forEach(leftKeys, function (leftNode, key) {
548
+ if (rightKeys[key] == null) {
549
+ var leftPath = path + '.' + leftNode.index
550
+ removeElement(
551
+ entityId,
552
+ leftPath,
553
+ childNodes[leftNode.index]
554
+ )
555
+ }
556
+ })
557
+
558
+ // Update nodes
559
+ forEach(rightKeys, function (rightNode, key) {
560
+ var leftNode = leftKeys[key]
561
+
562
+ // We only want updates for now
563
+ if (leftNode == null) return
564
+
565
+ var leftPath = path + '.' + leftNode.index
566
+
567
+ // Updated
568
+ positions[rightNode.index] = diffNode(
569
+ leftPath,
570
+ entityId,
571
+ leftNode,
572
+ rightNode,
573
+ childNodes[leftNode.index]
574
+ )
575
+ })
576
+
577
+ // Update the positions of all child components and event handlers
578
+ forEach(rightKeys, function (rightNode, key) {
579
+ var leftNode = leftKeys[key]
580
+
581
+ // We just want elements that have moved around
582
+ if (leftNode == null || leftNode.index === rightNode.index) return
583
+
584
+ var rightPath = path + '.' + rightNode.index
585
+ var leftPath = path + '.' + leftNode.index
586
+
587
+ // Update all the child component path positions to match
588
+ // the latest positions if they've changed. This is a bit hacky.
589
+ forEach(currentChildren, function (childId, childPath) {
590
+ if (leftPath === childPath) {
591
+ delete children[entityId][childPath]
592
+ children[entityId][rightPath] = childId
593
+ }
594
+ })
595
+ })
596
+
597
+ // Now add all of the new nodes last in case their path
598
+ // would have conflicted with one of the previous paths.
599
+ forEach(rightKeys, function (rightNode, key) {
600
+ var rightPath = path + '.' + rightNode.index
601
+ if (leftKeys[key] == null) {
602
+ positions[rightNode.index] = toNative(
603
+ entityId,
604
+ rightPath,
605
+ rightNode
606
+ )
607
+ }
608
+ })
609
+
610
+ } else {
611
+ var maxLength = Math.max(prev.children.length, next.children.length)
612
+
613
+ // Now diff all of the nodes that don't have keys
614
+ for (var i = 0; i < maxLength; i++) {
615
+ var leftNode = prev.children[i]
616
+ var rightNode = next.children[i]
617
+
618
+ // Removals
619
+ if (rightNode == null) {
620
+ removeElement(
621
+ entityId,
622
+ path + '.' + leftNode.index,
623
+ childNodes[leftNode.index]
624
+ )
625
+ }
626
+
627
+ // New Node
628
+ if (leftNode == null) {
629
+ positions[rightNode.index] = toNative(
630
+ entityId,
631
+ path + '.' + rightNode.index,
632
+ rightNode
633
+ )
634
+ }
635
+
636
+ // Updated
637
+ if (leftNode && rightNode) {
638
+ positions[leftNode.index] = diffNode(
639
+ path + '.' + leftNode.index,
640
+ entityId,
641
+ leftNode,
642
+ rightNode,
643
+ childNodes[leftNode.index]
644
+ )
645
+ }
646
+ }
647
+ }
648
+
649
+ // Reposition all the elements
650
+ forEach(positions, function (childEl, newPosition) {
651
+ var target = el.childNodes[newPosition]
652
+ if (childEl !== target) {
653
+ if (target) {
654
+ el.insertBefore(childEl, target)
655
+ } else {
656
+ el.appendChild(childEl)
657
+ }
658
+ }
659
+ })
660
+ }
661
+
662
+ /**
663
+ * Diff the attributes and add/remove them.
664
+ */
665
+
666
+ function diffAttributes (prev, next, el, entityId, path) {
667
+ var nextAttrs = next.attributes
668
+ var prevAttrs = prev.attributes
669
+
670
+ // add new attrs
671
+ forEach(nextAttrs, function (value, name) {
672
+ if (events[name] || !(name in prevAttrs) || prevAttrs[name] !== value) {
673
+ setAttribute(entityId, path, el, name, value)
674
+ }
675
+ })
676
+
677
+ // remove old attrs
678
+ forEach(prevAttrs, function (value, name) {
679
+ if (!(name in nextAttrs)) {
680
+ removeAttribute(entityId, path, el, name)
681
+ }
682
+ })
683
+ }
684
+
685
+ /**
686
+ * Update a component with the props from the next node. If
687
+ * the component type has changed, we'll just remove the old one
688
+ * and replace it with the new component.
689
+ */
690
+
691
+ function diffComponent (path, entityId, prev, next, el) {
692
+ if (next.component !== prev.component) {
693
+ return replaceElement(entityId, path, el, next)
694
+ } else {
695
+ var targetId = children[entityId][path]
696
+
697
+ // This is a hack for now
698
+ if (targetId) {
699
+ updateEntityProps(targetId, next.props)
700
+ }
701
+
702
+ return el
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Diff two element nodes.
708
+ */
709
+
710
+ function diffElement (path, entityId, prev, next, el) {
711
+ if (next.tagName !== prev.tagName) return replaceElement(entityId, path, el, next)
712
+ diffAttributes(prev, next, el, entityId, path)
713
+ diffChildren(path, entityId, prev, next, el)
714
+ return el
715
+ }
716
+
717
+ /**
718
+ * Removes an element from the DOM and unmounts and components
719
+ * that are within that branch
720
+ *
721
+ * side effects:
722
+ * - removes element from the DOM
723
+ * - removes internal references
724
+ *
725
+ * @param {String} entityId
726
+ * @param {String} path
727
+ * @param {HTMLElement} el
728
+ */
729
+
730
+ function removeElement (entityId, path, el) {
731
+ var childrenByPath = children[entityId]
732
+ var childId = childrenByPath[path]
733
+ var entityHandlers = handlers[entityId] || {}
734
+ var removals = []
735
+
736
+ // If the path points to a component we should use that
737
+ // components element instead, because it might have moved it.
738
+ if (childId) {
739
+ var child = entities[childId]
740
+ el = child.nativeElement
741
+ unmountEntity(childId)
742
+ removals.push(path)
743
+ } else {
744
+
745
+ // Just remove the text node
746
+ if (!isElement(el)) return el.parentNode.removeChild(el)
747
+
748
+ // Then we need to find any components within this
749
+ // branch and unmount them.
750
+ forEach(childrenByPath, function (childId, childPath) {
751
+ if (childPath === path || isWithinPath(path, childPath)) {
752
+ unmountEntity(childId)
753
+ removals.push(childPath)
754
+ }
755
+ })
756
+
757
+ // Remove all events at this path or below it
758
+ forEach(entityHandlers, function (fn, handlerPath) {
759
+ if (handlerPath === path || isWithinPath(path, handlerPath)) {
760
+ removeEvent(entityId, handlerPath)
761
+ }
762
+ })
763
+ }
764
+
765
+ // Remove the paths from the object without touching the
766
+ // old object. This keeps the object using fast properties.
767
+ forEach(removals, function (path) {
768
+ delete children[entityId][path]
769
+ })
770
+
771
+ // Remove it from the DOM
772
+ el.parentNode.removeChild(el)
773
+
774
+ // Return all of the elements in this node tree to the pool
775
+ // so that the elements can be re-used.
776
+ if (options.pooling) {
777
+ walk(el, function (node) {
778
+ if (!isElement(node) || !canPool(node.tagName)) return
779
+ getPool(node.tagName.toLowerCase()).push(node)
780
+ })
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Replace an element in the DOM. Removing all components
786
+ * within that element and re-rendering the new virtual node.
787
+ *
788
+ * @param {Entity} entity
789
+ * @param {String} path
790
+ * @param {HTMLElement} el
791
+ * @param {Object} vnode
792
+ *
793
+ * @return {void}
794
+ */
795
+
796
+ function replaceElement (entityId, path, el, vnode) {
797
+ var parent = el.parentNode
798
+ var index = Array.prototype.indexOf.call(parent.childNodes, el)
799
+
800
+ // remove the previous element and all nested components. This
801
+ // needs to happen before we create the new element so we don't
802
+ // get clashes on the component paths.
803
+ removeElement(entityId, path, el)
804
+
805
+ // then add the new element in there
806
+ var newEl = toNative(entityId, path, vnode)
807
+ var target = parent.childNodes[index]
808
+
809
+ if (target) {
810
+ parent.insertBefore(newEl, target)
811
+ } else {
812
+ parent.appendChild(newEl)
813
+ }
814
+
815
+ // update all `entity.nativeElement` references.
816
+ forEach(entities, function (entity) {
817
+ if (entity.nativeElement === el) {
818
+ entity.nativeElement = newEl
819
+ }
820
+ })
821
+
822
+ return newEl
823
+ }
824
+
825
+ /**
826
+ * Set the attribute of an element, performing additional transformations
827
+ * dependning on the attribute name
828
+ *
829
+ * @param {HTMLElement} el
830
+ * @param {String} name
831
+ * @param {String} value
832
+ */
833
+
834
+ function setAttribute (entityId, path, el, name, value) {
835
+ if (events[name]) {
836
+ addEvent(entityId, path, events[name], value)
837
+ return
838
+ }
839
+ switch (name) {
840
+ case 'value':
841
+ el.value = value
842
+ break
843
+ case 'innerHTML':
844
+ el.innerHTML = value
845
+ break
846
+ case svg.isAttribute(name):
847
+ el.setAttributeNS(svg.namespace, name, value)
848
+ break
849
+ default:
850
+ el.setAttribute(name, value)
851
+ break
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Remove an attribute, performing additional transformations
857
+ * dependning on the attribute name
858
+ *
859
+ * @param {HTMLElement} el
860
+ * @param {String} name
861
+ */
862
+
863
+ function removeAttribute (entityId, path, el, name) {
864
+ if (events[name]) {
865
+ removeEvent(entityId, path, events[name])
866
+ return
867
+ }
868
+ el.removeAttribute(name)
869
+ }
870
+
871
+ /**
872
+ * Checks to see if one tree path is within
873
+ * another tree path. Example:
874
+ *
875
+ * 0.1 vs 0.1.1 = true
876
+ * 0.2 vs 0.3.5 = false
877
+ *
878
+ * @param {String} target
879
+ * @param {String} path
880
+ *
881
+ * @return {Boolean}
882
+ */
883
+
884
+ function isWithinPath (target, path) {
885
+ return path.indexOf(target + '.') === 0
886
+ }
887
+
888
+ /**
889
+ * Is the DOM node an element node
890
+ *
891
+ * @param {HTMLElement} el
892
+ *
893
+ * @return {Boolean}
894
+ */
895
+
896
+ function isElement (el) {
897
+ return !!el.tagName
898
+ }
899
+
900
+ /**
901
+ * Get the pool for a tagName, creating it if it
902
+ * doesn't exist.
903
+ *
904
+ * @param {String} tagName
905
+ *
906
+ * @return {Pool}
907
+ */
908
+
909
+ function getPool (tagName) {
910
+ var pool = pools[tagName]
911
+ if (!pool) {
912
+ var poolOpts = svg.isElement(tagName) ?
913
+ { namespace: svg.namespace, tagName: tagName } :
914
+ { tagName: tagName }
915
+ pool = pools[tagName] = new Pool(poolOpts)
916
+ }
917
+ return pool
918
+ }
919
+
920
+ /**
921
+ * Clean up previously used native element for reuse.
922
+ *
923
+ * @param {HTMLElement} el
924
+ */
925
+
926
+ function cleanup (el) {
927
+ removeAllChildren(el)
928
+ removeAllAttributes(el)
929
+ return el
930
+ }
931
+
932
+ /**
933
+ * Remove all the attributes from a node
934
+ *
935
+ * @param {HTMLElement} el
936
+ */
937
+
938
+ function removeAllAttributes (el) {
939
+ for (var i = el.attributes.length - 1; i >= 0; i--) {
940
+ var name = el.attributes[i].name
941
+ el.removeAttribute(name)
942
+ }
943
+ }
944
+
945
+ /**
946
+ * Remove all the child nodes from an element
947
+ *
948
+ * @param {HTMLElement} el
949
+ */
950
+
951
+ function removeAllChildren (el) {
952
+ while (el.firstChild) el.removeChild(el.firstChild)
953
+ }
954
+
955
+ /**
956
+ * Trigger a hook on a component.
957
+ *
958
+ * @param {String} name Name of hook.
959
+ * @param {Entity} entity The component instance.
960
+ * @param {Array} args To pass along to hook.
961
+ */
962
+
963
+ function trigger (name, entity, args) {
964
+ if (typeof entity.component[name] !== 'function') return
965
+ entity.component[name].apply(null, args)
966
+ }
967
+
968
+ /**
969
+ * Update an entity to match the latest rendered vode. We always
970
+ * replace the props on the component when composing them. This
971
+ * will trigger a re-render on all children below this point.
972
+ *
973
+ * @param {Entity} entity
974
+ * @param {String} path
975
+ * @param {Object} vnode
976
+ *
977
+ * @return {void}
978
+ */
979
+
980
+ function updateEntityProps (entityId, nextProps) {
981
+ var entity = entities[entityId]
982
+ entity.pendingProps = nextProps
983
+ entity.dirty = true
984
+ invalidate()
985
+ }
986
+
987
+ /**
988
+ * Update component instance state.
989
+ */
990
+
991
+ function updateEntityState (entity, nextState) {
992
+ entity.pendingState = assign(entity.pendingState, nextState)
993
+ entity.dirty = true
994
+ invalidate()
995
+ }
996
+
997
+ /**
998
+ * Commit props and state changes to an entity.
999
+ */
1000
+
1001
+ function commit (entity) {
1002
+ entity.context = {
1003
+ state: entity.pendingState,
1004
+ props: entity.pendingProps,
1005
+ id: entity.id
1006
+ }
1007
+ entity.pendingState = assign({}, entity.context.state)
1008
+ entity.pendingProps = assign({}, entity.context.props)
1009
+ validateProps(entity.context.props, entity.propTypes)
1010
+ entity.dirty = false
1011
+ }
1012
+
1013
+ /**
1014
+ * Try to avoid creating new virtual dom if possible.
1015
+ *
1016
+ * Later we may expose this so you can override, but not there yet.
1017
+ */
1018
+
1019
+ function shouldUpdate (entity) {
1020
+ if (!entity.dirty) return false
1021
+ if (!entity.component.shouldUpdate) return true
1022
+ var nextProps = entity.pendingProps
1023
+ var nextState = entity.pendingState
1024
+ var bool = entity.component.shouldUpdate(entity.context, nextProps, nextState)
1025
+ return bool
1026
+ }
1027
+
1028
+ /**
1029
+ * Register an entity.
1030
+ *
1031
+ * This is mostly to pre-preprocess component properties and values chains.
1032
+ *
1033
+ * The end result is for every component that gets mounted,
1034
+ * you create a set of IO nodes in the network from the `value` definitions.
1035
+ *
1036
+ * @param {Component} component
1037
+ */
1038
+
1039
+ function register (entity) {
1040
+ var component = entity.component
1041
+ // all entities for this component type.
1042
+ var entities = component.entities = component.entities || {}
1043
+ // add entity to component list
1044
+ entities[entity.id] = entity
1045
+
1046
+ // get 'class-level' sources.
1047
+ var sources = component.sources
1048
+ if (sources) return
1049
+
1050
+ var map = component.sourceToPropertyName = {}
1051
+ component.sources = sources = []
1052
+ var propTypes = component.propTypes
1053
+ for (var name in propTypes) {
1054
+ var data = propTypes[name]
1055
+ if (!data) continue
1056
+ if (!data.source) continue
1057
+ sources.push(data.source)
1058
+ map[data.source] = name
1059
+ }
1060
+
1061
+ // send value updates to all component instances.
1062
+ sources.forEach(function (source) {
1063
+ connections[source] = update
1064
+
1065
+ function update (data) {
1066
+ var prop = map[source]
1067
+ for (var entityId in entities) {
1068
+ var entity = entities[entityId]
1069
+ var changes = {}
1070
+ changes[prop] = data
1071
+ updateEntityProps(entityId, assign(entity.pendingProps, changes))
1072
+ }
1073
+ }
1074
+ })
1075
+ }
1076
+
1077
+ /**
1078
+ * Set the initial source value on the entity
1079
+ *
1080
+ * @param {Entity} entity
1081
+ */
1082
+
1083
+ function setSources (entity) {
1084
+ var component = entity.component
1085
+ var map = component.sourceToPropertyName
1086
+ var sources = component.sources
1087
+ sources.forEach(function (source) {
1088
+ var name = map[source]
1089
+ if (entity.pendingProps[name] != null) return
1090
+ entity.pendingProps[name] = app.sources[source] // get latest value plugged into global store
1091
+ })
1092
+ }
1093
+
1094
+ /**
1095
+ * Add all of the DOM event listeners
1096
+ */
1097
+
1098
+ function addNativeEventListeners () {
1099
+ forEach(events, function (eventType) {
1100
+ document.body.addEventListener(eventType, handleEvent, true)
1101
+ })
1102
+ }
1103
+
1104
+ /**
1105
+ * Add all of the DOM event listeners
1106
+ */
1107
+
1108
+ function removeNativeEventListeners () {
1109
+ forEach(events, function (eventType) {
1110
+ document.body.removeEventListener(eventType, handleEvent, true)
1111
+ })
1112
+ }
1113
+
1114
+ /**
1115
+ * Handle an event that has occured within the container
1116
+ *
1117
+ * @param {Event} event
1118
+ */
1119
+
1120
+ function handleEvent (event) {
1121
+ var target = event.target
1122
+ var entityId = target.__entity__
1123
+ var eventType = event.type
1124
+
1125
+ // Walk up the DOM tree and see if there is a handler
1126
+ // for this event type higher up.
1127
+ while (target && target.__entity__ === entityId) {
1128
+ var fn = keypath.get(handlers, [entityId, target.__path__, eventType])
1129
+ if (fn) {
1130
+ event.delegateTarget = target
1131
+ fn(event)
1132
+ break
1133
+ }
1134
+ target = target.parentNode
1135
+ }
1136
+ }
1137
+
1138
+ /**
1139
+ * Bind events for an element, and all it's rendered child elements.
1140
+ *
1141
+ * @param {String} path
1142
+ * @param {String} event
1143
+ * @param {Function} fn
1144
+ */
1145
+
1146
+ function addEvent (entityId, path, eventType, fn) {
1147
+ keypath.set(handlers, [entityId, path, eventType], throttle(function (e) {
1148
+ var entity = entities[entityId]
1149
+ if (entity) {
1150
+ fn.call(null, e, entity.context, setState(entity))
1151
+ } else {
1152
+ fn.call(null, e)
1153
+ }
1154
+ }))
1155
+ }
1156
+
1157
+ /**
1158
+ * Unbind events for a entityId
1159
+ *
1160
+ * @param {String} entityId
1161
+ */
1162
+
1163
+ function removeEvent (entityId, path, eventType) {
1164
+ var args = [entityId]
1165
+ if (path) args.push(path)
1166
+ if (eventType) args.push(eventType)
1167
+ keypath.del(handlers, args)
1168
+ }
1169
+
1170
+ /**
1171
+ * Unbind all events from an entity
1172
+ *
1173
+ * @param {Entity} entity
1174
+ */
1175
+
1176
+ function removeAllEvents (entityId) {
1177
+ keypath.del(handlers, [entityId])
1178
+ }
1179
+
1180
+ /**
1181
+ * Validate the current properties. These simple validations
1182
+ * make it easier to ensure the correct props are passed in.
1183
+ *
1184
+ * Available rules include:
1185
+ *
1186
+ * type: string | array | object | boolean | number | date | function
1187
+ * expects: [] An array of values this prop could equal
1188
+ * optional: Boolean
1189
+ */
1190
+
1191
+ function validateProps (props, rules) {
1192
+ if (!options.validateProps) return
1193
+
1194
+ // TODO: Only validate in dev mode
1195
+ forEach(rules, function (options, name) {
1196
+ if (name === 'children') return
1197
+ var value = props[name]
1198
+ var optional = (options.optional === true)
1199
+ if (optional && value == null) {
1200
+ return
1201
+ }
1202
+ if (!optional && value == null) {
1203
+ throw new Error('Missing prop named: ' + name)
1204
+ }
1205
+ if (options.type && type(value) !== options.type) {
1206
+ throw new Error('Invalid type for prop named: ' + name)
1207
+ }
1208
+ if (options.expects && options.expects.indexOf(value) < 0) {
1209
+ throw new Error('Invalid value for prop named: ' + name + '. Must be one of ' + options.expects.toString())
1210
+ }
1211
+ })
1212
+
1213
+ // Now check for props that haven't been defined
1214
+ forEach(props, function (value, key) {
1215
+ if (key === 'children') return
1216
+ if (!rules[key]) throw new Error('Unexpected prop named: ' + key)
1217
+ })
1218
+ }
1219
+
1220
+ /**
1221
+ * Used for debugging to inspect the current state without
1222
+ * us needing to explicitly manage storing/updating references.
1223
+ *
1224
+ * @return {Object}
1225
+ */
1226
+
1227
+ function inspect () {
1228
+ return {
1229
+ entities: entities,
1230
+ pools: pools,
1231
+ handlers: handlers,
1232
+ connections: connections,
1233
+ currentElement: currentElement,
1234
+ options: options,
1235
+ app: app,
1236
+ container: container,
1237
+ children: children
1238
+ }
1239
+ }
1240
+
1241
+ /**
1242
+ * Return an object that lets us completely remove the automatic
1243
+ * DOM rendering and export debugging tools.
1244
+ */
1245
+
1246
+ return {
1247
+ remove: teardown,
1248
+ inspect: inspect
1249
+ }
1250
+ }
1251
+
1252
+ /**
1253
+ * A rendered component instance.
1254
+ *
1255
+ * This manages the lifecycle, props and state of the component.
1256
+ * It's basically just a data object for more straightfoward lookup.
1257
+ *
1258
+ * @param {Component} component
1259
+ * @param {Object} props
1260
+ */
1261
+
1262
+ function Entity (component, props) {
1263
+ this.id = uid()
1264
+ this.component = component
1265
+ this.propTypes = component.propTypes || {}
1266
+ this.context = {}
1267
+ this.context.id = this.id;
1268
+ this.context.props = defaults(props || {}, component.defaultProps || {})
1269
+ this.context.state = this.component.initialState ? this.component.initialState() : {}
1270
+ this.pendingProps = assign({}, this.context.props)
1271
+ this.pendingState = assign({}, this.context.state)
1272
+ this.dirty = false
1273
+ this.virtualElement = null
1274
+ this.nativeElement = null
1275
+ this.displayName = component.name || 'Component'
1276
+ }
1277
+
1278
+ /**
1279
+ * Should we pool an element?
1280
+ */
1281
+
1282
+ function canPool(tagName) {
1283
+ return avoidPooling.indexOf(tagName) < 0
1284
+ }
1285
+
1286
+ /**
1287
+ * Get a nested node using a path
1288
+ *
1289
+ * @param {HTMLElement} el The root node '0'
1290
+ * @param {String} path The path string eg. '0.2.43'
1291
+ */
1292
+
1293
+ function getNodeAtPath(el, path) {
1294
+ var parts = path.split('.')
1295
+ parts.shift()
1296
+ while (parts.length) {
1297
+ el = el.childNodes[parts.pop()]
1298
+ }
1299
+ return el
1300
+ }