;
131 | optional?: boolean;
132 |
133 | /**
134 | * A source string links the value of a property to a value which
135 | * must be present in the Application environment.
136 | */
137 | source?: string;
138 | }
139 |
140 | /**
141 | * A descriptor of Component creation, behavior, and lifecycle.
142 | *
143 | * Deku's primary interface is operated via specification of components.
144 | * Components are collections of behavior and state parameterized by immutable,
145 | * static properties. A value of type Spec specifies a component of
146 | * type Component
with properties in type P and local state in type S.
147 | */
148 | interface Spec
{
149 |
150 | /** Define a name for the component that can be used in debugging */
151 | name?: string;
152 |
153 | /** Validate the props sent to your component */
154 | propTypes?: { [prop: string]: PropSpec };
155 |
156 | /**
157 | * Render a component. We need to pass in setState so that callbacks on
158 | * sub-components. This may change in the future.
159 | *
160 | * Client: Yes
161 | * Server: Yes
162 | */
163 | render: (component: Component
, setState: (newState: S) => void) => VirtualNode
;
164 |
165 | /**
166 | * Get the initial state for the component. We don't pass props in here like
167 | * React does because the state should just be computed in the render function.
168 | */
169 | initialState?: () => S;
170 |
171 | /** Default props can be defined that will be used across all instances. */
172 | defaultProps?: P;
173 |
174 | /** This is called on both the server and the client. */
175 | beforeMount?: (component: Component
) => any;
176 |
177 | /**
178 | * This is called after the component is rendered the first time and is only
179 | * ever called once.
180 | *
181 | * Use cases:
182 | * - Analytics tracking
183 | * - Loading initial data
184 | * - Setting the state that should change immediately eg. open/close
185 | * - Adding DOM event listeners on the window/document
186 | * - Moving the element in the DOM. eg. to the root for dialogs
187 | * - Focusing the element
188 | *
189 | * Client: Yes
190 | * Server: No
191 | */
192 | afterMount?: (component: Component
, el: Node, setState: (newState: S) => void) => any;
193 |
194 | /**
195 | * This is called once just before the element is removed. It should be used
196 | * to clean up after the component.
197 | *
198 | * Use cases:
199 | * - Unbind window/document event handlers
200 | * - Edit the DOM in anyway to clean up after the component
201 | * - Unbind any event emitters
202 | * - Disconnect streams
203 | *
204 | * Client: Yes
205 | * Server: No
206 | */
207 | beforeUnmount?: (component: Component
, el: Node) => any;
208 |
209 | /**
210 | * This is called on each update and can be used to skip renders to improve
211 | * performance of the component.
212 | */
213 | shouldUpdate?: (component: Component
, nextProps: P, nextState: S) => boolean;
214 |
215 | /**
216 | * Called before each render on both the client and server.
217 | *
218 | * Example use cases:
219 | * - Updating stream/emitter based on next props
220 | *
221 | * Client: Yes
222 | * Server: Yes
223 | */
224 | beforeRender?: (component: Component
) => any;
225 |
226 | /**
227 | * Called after every render, including the first one. This is better
228 | * than the afterUpdate as it's called on the first render so if forces
229 | * us to think in single renders instead of worrying about the lifecycle.
230 | * It can't update state here because then you'd be changing state based on
231 | * the DOM.
232 | *
233 | * Example use cases:
234 | * - Update the DOM based on the latest state eg. animations, event handlers
235 | *
236 | * Client: Yes
237 | * Server: No
238 | */
239 | afterRender?: (component: Component
, el: Node) => any;
240 |
241 | /**
242 | * This isn't called on the first render only on updates.
243 | *
244 | * Example use cases:
245 | * - Updating stream/emitter based on next props
246 | *
247 | * Client: Yes
248 | * Server: No
249 | */
250 | beforeUpdate?: (component: Component
, nextProps: P, nextState: S) => any;
251 |
252 | /**
253 | * Not called on the first render but on any update.
254 | *
255 | * Example use cases:
256 | * - Changing the state based on the previous state transition
257 | * - Calling callbacks when a state change happens
258 | *
259 | * Client: Yes
260 | * Server: No
261 | */
262 | afterUpdate?: (component: Component
, prevProps: P, prevState: S, setState: (newState: S) => void) => void;
263 | }
264 |
265 |
266 |
267 | // ANCILARY PROPERTY INTERFACES
268 | // --------------------------------------------------------------------
269 |
270 | /**
271 | * Prop types are assigned a slot to hold the children assigned to a component
272 | * after it has been built into the virtual tree; therefore, all Prop types
273 | * should recognize that they may also include a children slot.
274 | */
275 | interface HasChildren {
276 | children?: Array>;
277 | }
278 |
279 | /**
280 | * A component with Keyed properties can be more efficiently diffed under
281 | * the assumption that keys preserve identity between diffs.
282 | *
283 | * To be more particular, when diffing old and new virtual elements that
284 | * and children of the new element with keys that match the keys of the old
285 | * element are actually *the same* nodes which have perhaps merely moved.
286 | * Without this identity preservation over keys it would be necessary to
287 | * completely remove and replace all children more frequently.
288 | */
289 | interface Keyed {
290 | key?: string;
291 | }
292 |
293 | /**
294 | * An Evented property may contain keys corresponding to DOM events. The
295 | * EventListeners stored at these keys will be registered against the DOM
296 | * element corresponding to the virtual element with the given property.
297 | *
298 | * In Deku the events submitted are regular browser events---there is no
299 | * synthetic event system for canonicalization of browser event semantics.
300 | */
301 |
302 | interface EventListenerOf {
303 | (event: T): any;
304 | }
305 |
306 | // For listing and event details, see
307 | //
308 | interface Evented {
309 |
310 | // Element interaction events
311 | onFocus?: EventListenerOf;
312 | onBlur?: EventListenerOf;
313 | onChange?: EventListenerOf;
314 | onInput?: EventListenerOf;
315 |
316 | // Clipboard events
317 | //
318 | // Ought to be
319 | onCopy?: EventListenerOf;
320 | onCut?: EventListenerOf;
321 | onPaste?: EventListenerOf;
322 |
323 | // Drag events
324 | onDrag?: EventListenerOf;
325 | onDragEnd?: EventListenerOf;
326 | onDragEnter?: EventListenerOf;
327 | onDragExit?: EventListenerOf;
328 | onDragLeave?: EventListenerOf;
329 | onDragOver?: EventListenerOf
330 | onDragStart?: EventListenerOf;
331 | onDrop?: EventListenerOf;
332 |
333 | // UI events
334 | onScroll?: EventListenerOf;
335 |
336 | // Keyboard events
337 | onKeyDown?: EventListenerOf;
338 | onKeyUp?: EventListenerOf;
339 |
340 | // Mouse events
341 | onClick?: EventListenerOf;
342 | onDoubleClick?: EventListenerOf;
343 | onContextMenu?: EventListenerOf;
344 | onMouseDown?: EventListenerOf;
345 | onMouseMove?: EventListenerOf;
346 | onMouseOut?: EventListenerOf;
347 | onMouseOver?: EventListenerOf;
348 | onMouseUp?: EventListenerOf;
349 |
350 | // Form events
351 | onSubmit?: EventListenerOf;
352 |
353 | // Touch events
354 | //
355 | // Ought to be s
356 | onTouchCancel?: EventListenerOf;
357 | onTouchEnd?: EventListenerOf;
358 | onTouchMove?: EventListenerOf;
359 | onTouchStart?: EventListenerOf;
360 | }
361 |
362 | }
363 |
364 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 |
2 | 1.0.0 / 2015-12-04
3 | ==================
4 |
5 | * Add load event
6 | * fix counter example
7 | * Fixed bug with adding events
8 | * Add HTML5 media events
9 | * add babel 6 instructions yet point back to babel@5
10 | * alias npm test script to make test for sake of convention
11 | * add to install guide using webpack
12 | * Don't treat number as a falsy attribute
13 | * Add failing test for input values
14 | * getting examples working using duo-serve
15 |
16 | 0.5.6 / 2015-09-21
17 | ==================
18 |
19 | * meta: updating dist for duo/component
20 |
21 | 0.5.5 / 2015-09-11
22 | ==================
23 |
24 | * return result of calling event handler (return `false` to cancel bubbling)
25 |
26 | 0.5.4 / 2015-08-14
27 | ==================
28 |
29 | * Merge pull request #232 from dekujs/stringify/empty-attrs
30 | * properly handling empty attrs, with tests to prevent regression
31 | * Merge pull request #231 from dekujs/stringify/no-children
32 | * fixing stringify for components with children
33 | * Update events.js
34 |
35 | 0.5.3 / 2015-08-12
36 | ==================
37 |
38 | * Merge pull request #230 from dekujs/text-selection-active-only
39 | * ie handles focus differently, so gotta make sure we account for that
40 | * only adjust text selection for the active element
41 |
42 | 0.5.2 / 2015-08-12
43 | ==================
44 |
45 | * Fixed issue selecting text with some input types
46 | * Update history. I'll fix that makefile one day
47 |
48 | 0.5.1 / 2015-08-11
49 | ==================
50 |
51 | * adding some helpful form-related events
52 | * Corrected afterMount arity. Fixes #225.
53 | * Keep cursor position when changing input value
54 |
55 | 0.5.0 / 2015-08-03
56 | ==================
57 |
58 | * Adding a heuristic to determine the best HTMLElement on which to attach events listener. The intention here is to enable deku to render into document fragments such as Shadow DOM.
59 | * Components can be functions instead of objects
60 | * Update dependencies
61 | * Fixed bug with replacing text nodes with undefined
62 | * Refactored the tests
63 | * Removed DOM pooling
64 | * Switch to use virtual-element
65 |
66 | 0.4.12 / 2015-07-28
67 | ==================
68 |
69 | * Merge pull request #212 from dekujs/flatmap-children
70 | * Fixed failing tests
71 | * Update index.js
72 | * Flattening the virtual element children array
73 |
74 | 0.4.11 / 2015-07-17
75 | ==================
76 |
77 | * Fixed deprecation warnings
78 | * Updated history
79 |
80 | 0.4.10 / 2015-07-16
81 | ==================
82 |
83 | * Added validate hook
84 | * Attach events to document instead of document.body
85 | * added ability to cancel events
86 | * Possibility to pass `false` as well as `null` in component children
87 | * Remove prop validation
88 | * Added deprecation warnings for magic class and style transformations.
89 | * No longer flattening children in virtual nodes
90 | * Faster SVG element lookups
91 |
92 | 0.4.9 / 2015-07-07
93 | ==================
94 |
95 | * Merge pull request #191 from timmak/svg-missing-animate
96 | * Add animate to svg list
97 |
98 | 0.4.8 / 2015-07-01
99 | ==================
100 |
101 | * Merge branch 'master' of ssh://github.com/dekujs/deku
102 | * Merge pull request #188 from xdissent/fix-remove-null-el
103 | * Handle null element in isElement(). fixes #180
104 |
105 | 0.4.7 / 2015-07-01
106 | ==================
107 |
108 | * Not pooling select elements
109 |
110 | 0.4.6 / 2015-06-29
111 | ==================
112 |
113 | * Merge pull request #187 from dekujs/should-render
114 | * Fixed: State not committed during shouldUpdate
115 | * Merge pull request #177 from mpal9000/patch-1
116 | * docs - initialState props
117 |
118 | 0.4.5 / 2015-06-13
119 | ==================
120 |
121 | * We made it smaller!
122 | * Merge pull request #173 from foray1010/master
123 | * Added wheel event
124 | * Update README.md
125 | * Update README.md
126 | * Merge pull request #166 from xdissent/patch-1
127 | * Merge pull request #167 from DylanPiercey/patch-1
128 | * Update jsx.md
129 | * Update events link in README
130 |
131 | 0.4.4 / 2015-06-05
132 | ==================
133 |
134 | * Added `createElement` alias for `element`
135 | * Update components.md
136 | * Updated changelog
137 |
138 | 0.4.3 / 2015-06-04
139 | ==================
140 |
141 | * Remove event throttling. Fixes #159
142 | * added keypress event
143 | * Fixed issue with rendering event handlers when using renderString
144 |
145 | 0.4.2 / 2015-05-28
146 | ==================
147 |
148 | * fixed event handling so events bubble to parent handlers
149 |
150 | 0.4.1 / 2015-05-26
151 | ==================
152 |
153 | * propTypes validation - support for array of types, type as function
154 |
155 | 0.4.0 / 2015-05-22
156 | ==================
157 |
158 | * Fixed: Fixed issue with components rendered as root nodes.
159 | * New: initialState now takes the props as a param
160 | * New: afterMount, afterRender and event handlers can now return a promise. This means you can use ES7 async functions to have pure lifecycle functions too.
161 | * New: You can nest propTypes now. Just set the `type` field to be another propTypes object.
162 | * Fixed: `afterRender` and `afterMount` are now called when the element is in the DOM.
163 | * Updated: Added phantomjs to the dev deps
164 |
165 | 0.3.3 / 2015-05-22
166 | ==================
167 |
168 | * Added mouseenter/mouseleave
169 | * Merge pull request #137 from Frikki/issue-134/modular-fastjs
170 | * Replaced fast.js require with modular requirement.
171 |
172 | 0.3.2 / 2015-05-20
173 | ==================
174 |
175 |
176 |
177 | 0.3.1 / 2015-05-21
178 | ==================
179 |
180 | * fixed error with swapping component using sources
181 |
182 | 0.3.0 / 2015-05-18
183 | ==================
184 |
185 | * Added: warnings and nicer error messages
186 | * Added: Always emptying the container when rendering
187 | * Added: Deku.d.ts file
188 | * Removed: the `defaultX` attributes from checkboxes, selects and inputs
189 | * Fixed: rendering for `checked`, `selected` and `disabled` attributes
190 | * Fixed: multiple components depending on the same `source`
191 |
192 | 0.2.17 / 2015-05-11
193 | ==================
194 |
195 | * set sources on update
196 |
197 | 0.2.16 / 2015-05-11
198 | ==================
199 |
200 | * Using a different component object each render
201 | * Cleaned up tests and build
202 | * Calling .set will always trigger an update instead of checking equality with the previous data value.
203 | * Added React comparison examples
204 | * Fixed bug where handler references weren't removed
205 | * Skip rendering if element is the same
206 |
207 | 0.2.15 / 2015-05-04
208 | ==================
209 |
210 | * add svg support
211 | * Throw errors for empty types on elements
212 |
213 | 0.2.14 / 2015-05-03
214 | ==================
215 |
216 | * Using fast.js
217 | * Added some simple examples
218 |
219 | 0.2.13 / 2015-04-30
220 | ==================
221 |
222 | * Added workaround for diffChildren bug
223 |
224 | 0.2.12 / 2015-04-29
225 | ==================
226 |
227 | * Merge pull request #83 from segmentio/fix/child-key-diffing
228 | * Removed key diffing for elements
229 | * Passing tests for the keys with events
230 | * Tests passing with janky first version
231 | * Added failing test
232 | * Only flatten children one level deep
233 |
234 | 0.2.11 / 2015-04-29
235 | ==================
236 |
237 | * Added test for virtual node indexes
238 | * Correctly casting key to a string
239 |
240 | 0.2.10 / 2015-04-29
241 | ==================
242 |
243 | * Improved performance by removing 'omit'
244 | * IE10 fix
245 | * Running all tests
246 | * Added tests for components with keys
247 | * Coercing keys to strings
248 | * Code style
249 |
250 | 0.2.9 / 2015-04-28
251 | ==================
252 |
253 | * Passing tests
254 | * The patch returns the updated element
255 | * Code style
256 | * Removing elements first when diffing
257 | * Cleaned up the key diffing
258 |
259 | 0.2.8 / 2015-04-28
260 | ==================
261 |
262 | * Fixed more issues with falsy keys
263 |
264 | 0.2.7 / 2015-04-28
265 | ==================
266 |
267 | * Fixed falsy keys
268 |
269 | 0.2.6 / 2015-04-28
270 | ==================
271 |
272 | * Fixed incorrect path
273 |
274 | 0.2.5 / 2015-04-28
275 | ==================
276 |
277 | * Avoid touching elements that haven't moved
278 | * Added test for adding nodes with new keys
279 |
280 | 0.2.4 / 2015-04-28
281 | ==================
282 |
283 | * Fixed bug with creating new nodes in the diff
284 |
285 | 0.2.3 / 2015-04-27
286 | ==================
287 |
288 | * Fixed issue with initial mount
289 |
290 | 0.2.2 / 2015-04-27
291 | ==================
292 |
293 | * Merge branch 'docs'
294 | * Updated docs
295 | * Removed old docs and examples
296 | * Allowing easier initial mounting
297 | * Adding docs
298 |
299 | 0.2.1 / 2015-04-25
300 | ==================
301 |
302 | * Fixed bug with diffing keyed nodes
303 |
304 | 0.2.0 / 2015-04-25
305 | ==================
306 |
307 | * Updated the hook API
308 | * Removed defaults
309 |
310 | 0.1.1 / 2015-04-22
311 | ==================
312 |
313 | * Replaced lodash and removed unused modules
314 |
315 | 0.1.0 / 2015-04-21
316 | ==================
317 |
318 | Breaking
319 | * Updated the top-level API. It now mounts virtual nodes instead of components directly.
320 | * Removed the `component()` DSL. Components are just objects now with no concept of `this`. This is one step towards making hook functions pure.
321 | * There is no more `this` in any of the functions used in a component. Instead of `this.setState`, the last argument to the function is `setState`, or `send` (think of it as sending changes to the UI).
322 | * Removed tagName parsing (eg. `dom('div.foo')`) as it was slowing things down
323 |
324 | New Features
325 | * Added key diffing using the `key` attribute on virtual nodes
326 | * Added optional prop validation via `propTypes`
327 | * Added defaultProps hook to components
328 | * Added the ability for components to access data on the app. This makes it easy to side-load data.
329 |
330 | Fixes
331 | * Fixed bug with inputs/textarea being pooled incorrectly
332 | * Merge pull request #72 from segmentio/attr-modification-bug
333 | * Fixed a bug in the renderer for falsy attributes
334 | * Numerous speed improvements
335 | * Fixed bug with string renderer not calling `beforeMount`
336 | * Removed the raf loop and just batches
337 |
338 | 0.0.33 / 2015-04-02
339 | ==================
340 |
341 | * Fixed bug with nested components not being unmounted
342 | * Added test for nested components disabling pooling
343 |
344 | 0.0.32 / 2015-04-01
345 | ==================
346 |
347 | * dom: Fixed disablePooling flag for nested components
348 |
349 | 0.0.31 / 2015-04-01
350 | ==================
351 |
352 | * dom: Tests for scene removal
353 | * dom: Cleaned up removing of elements
354 |
355 | 0.0.30 / 2015-03-27
356 | ==================
357 |
358 | * Added DOM pooling
359 |
360 | 0.0.29 / 2015-03-24
361 | ==================
362 |
363 | * Breaking change: Updated the scene/renderer API to allow for more powerful plugins. The Component API is now decoupled from the renderer.
364 | * Tests now using ES6
365 | * Fixed beforeMount not firing with renderString
366 | * Fixed innerHTML rendering with renderString
367 |
368 | 0.0.28 / 2015-03-11
369 | ==================
370 |
371 | * Interactions bind to the body
372 | * Update component#render() to throw if container is empty
373 | * Fixed tests in IE9. Fixed SauceLab tests.
374 | * Removed all the crap from the repo
375 |
376 | 0.0.27 / 2015-02-26
377 | ==================
378 |
379 | * Fixed bug with re-rendering child nodes
380 |
381 | 0.0.26 / 2015-02-26
382 | ==================
383 |
384 | * The renderer now renders the entire tree whenever it is dirty and no longer performs shallow equality checks to determine if a component should update. This means that when a component changes, the entire tree below it is re-rendered, including all nested components. This helps to prevent annoying bugs with objects changing and the UI not updating.
385 | * The scene continues to update on every frame, but will still only actually render a component in the tree has changed.
386 | * There is a new shouldUpdate hook available to components to optimize this. You can stop it from re-rendering a component by returning false from this method.
387 | * Removed channels from the API. This was an experimental API and it turned out to be the wrong abstraction when using it in practice. It was making the library responsible for more than it should be.
388 | * The entities now don't know about the scene at all, making them completely decoupled from it.
389 | * The HTMLRenderer now keeps track of the entity state and structure. This allows the entity to become a wrapper around the user component and only provide managing the state/props from the component.
390 | * The scene will now pause when there is an error during rendering to prevent endless errors.
391 | * The scene methods no longer return a promise. It was never used in practice because the top level components are never used in flow control.
392 | * The diff is now slightly more decoupled, which will allow it to be extracted from deku.
393 | * Removed some unused dependencies. This should make the whole library smaller.
394 | * The logic around commiting changes to props/state in the entity has been reworked. It's now much simpler and less prone to bugs.
395 |
396 |
397 | 0.0.25 / 2015-02-22
398 | ==================
399 |
400 | * JSX support
401 |
402 | 0.0.24 / 2015-02-11
403 | ==================
404 |
405 | * Added failing test for nested events
406 | * added hasFunction to fix #47
407 | * added failing test to demo function diffing
408 |
409 | 0.0.23 / 2015-02-09
410 | ==================
411 |
412 | * Added innerHTML support
413 | * Fixed drag and drop test event
414 | * Added test for #47
415 |
416 | 0.0.22 / 2015-02-03
417 | ==================
418 |
419 | * Pulled virtual DOM lib out
420 |
421 | 0.0.21 / 2015-02-03
422 | ==================
423 |
424 | * Added ability to render component at the root
425 |
426 | 0.0.20 / 2015-01-29
427 | ==================
428 |
429 | * Value attribute gets a special case in the diff
430 | * Using raf-loop instead of local module
431 | * Using uid module
432 |
433 | 0.0.19 / 2015-01-25
434 | ==================
435 |
436 | * Moved to browserify for the build
437 |
438 | 0.0.18 / 2015-01-25
439 | ==================
440 |
441 | * Fixed event delegation
442 | * Added some super basic perf tests
443 | * Fixed issue with scene not removing listeners
444 |
445 | 0.0.17 / 2015-01-24
446 | ==================
447 |
448 | * Fixed bug when changing root node. Closes #33
449 |
450 | 0.0.16 / 2015-01-23
451 | ==================
452 |
453 | * Fixed issue with channels not being sent to render
454 |
455 | 0.0.15 / 2015-01-23
456 | ==================
457 |
458 | * Added .channel and .prop methods
459 | * Removed .send and .onMessage in favour of channels
460 | * Scene is no longer immediately rendered
461 |
462 | 0.0.14 / 2015-01-21
463 | ==================
464 |
465 | * Add .send and .onMessage methods. You can call this.send(name, payload) within components and listen for those events on the scene with scene.onMessage(name, fn);
466 |
467 | 0.0.13 / 2015-01-20
468 | ==================
469 |
470 | * Fixed a bug with IDs being identical
471 | * Added History.md
472 |
473 | 0.0.12 / 2015-01-19
474 | ==================
475 |
476 | * Add repo field
477 | * Updated bump
478 | * Updated release task
479 |
--------------------------------------------------------------------------------
/lib/render.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Dependencies.
3 | */
4 |
5 | var raf = require('component-raf')
6 | var isDom = require('is-dom')
7 | var uid = require('get-uid')
8 | var keypath = require('object-path')
9 | var events = require('./events')
10 | var svg = require('./svg')
11 | var defaults = require('object-defaults')
12 | var forEach = require('fast.js/forEach')
13 | var assign = require('fast.js/object/assign')
14 | var reduce = require('fast.js/reduce')
15 | var nodeType = require('./node-type')
16 |
17 | /**
18 | * Expose `dom`.
19 | */
20 |
21 | module.exports = render
22 |
23 | /**
24 | * Render an app to the DOM
25 | *
26 | * @param {Application} app
27 | * @param {HTMLElement} container
28 | * @param {Object} opts
29 | *
30 | * @return {Object}
31 | */
32 |
33 | function render (app, container, opts) {
34 | var frameId
35 | var isRendering
36 | var rootId = 'root'
37 | var currentElement
38 | var currentNativeElement
39 | var connections = {}
40 | var components = {}
41 | var entities = {}
42 | var handlers = {}
43 | var mountQueue = []
44 | var children = {}
45 | children[rootId] = {}
46 |
47 | if (!isDom(container)) {
48 | throw new Error('Container element must be a DOM element')
49 | }
50 |
51 | /**
52 | * Rendering options. Batching is only ever really disabled
53 | * when running tests, and pooling can be disabled if the user
54 | * is doing something stupid with the DOM in their components.
55 | */
56 |
57 | var options = defaults(assign({}, app.options || {}, opts || {}), {
58 | batching: true
59 | })
60 |
61 | /**
62 | * Listen to DOM events
63 | */
64 | var rootElement = getRootElement(container)
65 | addNativeEventListeners()
66 |
67 | /**
68 | * Watch for changes to the app so that we can update
69 | * the DOM as needed.
70 | */
71 |
72 | app.on('unmount', onunmount)
73 | app.on('mount', onmount)
74 | app.on('source', onupdate)
75 |
76 | /**
77 | * If the app has already mounted an element, we can just
78 | * render that straight away.
79 | */
80 |
81 | if (app.element) render()
82 |
83 | /**
84 | * Teardown the DOM rendering so that it stops
85 | * rendering and everything can be garbage collected.
86 | */
87 |
88 | function teardown () {
89 | removeNativeEventListeners()
90 | removeNativeElement()
91 | app.off('unmount', onunmount)
92 | app.off('mount', onmount)
93 | app.off('source', onupdate)
94 | }
95 |
96 | /**
97 | * Swap the current rendered node with a new one that is rendered
98 | * from the new virtual element mounted on the app.
99 | *
100 | * @param {VirtualElement} element
101 | */
102 |
103 | function onmount () {
104 | invalidate()
105 | }
106 |
107 | /**
108 | * If the app unmounts an element, we should clear out the current
109 | * rendered element. This will remove all the entities.
110 | */
111 |
112 | function onunmount () {
113 | removeNativeElement()
114 | currentElement = null
115 | }
116 |
117 | /**
118 | * Update all components that are bound to the source
119 | *
120 | * @param {String} name
121 | * @param {*} data
122 | */
123 |
124 | function onupdate (name, data) {
125 | if (!connections[name]) return
126 | connections[name].forEach(function (update) {
127 | update(data)
128 | })
129 | }
130 |
131 | /**
132 | * Render and mount a component to the native dom.
133 | *
134 | * @param {Entity} entity
135 | * @return {HTMLElement}
136 | */
137 |
138 | function mountEntity (entity) {
139 | register(entity)
140 | setSources(entity)
141 | children[entity.id] = {}
142 | entities[entity.id] = entity
143 |
144 | // commit initial state and props.
145 | commit(entity)
146 |
147 | // callback before mounting.
148 | trigger('beforeMount', entity, [entity.context])
149 | trigger('beforeRender', entity, [entity.context])
150 |
151 | // render virtual element.
152 | var virtualElement = renderEntity(entity)
153 | // create native element.
154 | var nativeElement = toNative(entity.id, '0', virtualElement)
155 |
156 | entity.virtualElement = virtualElement
157 | entity.nativeElement = nativeElement
158 |
159 | // Fire afterRender and afterMount hooks at the end
160 | // of the render cycle
161 | mountQueue.push(entity.id)
162 |
163 | return nativeElement
164 | }
165 |
166 | /**
167 | * Remove a component from the native dom.
168 | *
169 | * @param {Entity} entity
170 | */
171 |
172 | function unmountEntity (entityId) {
173 | var entity = entities[entityId]
174 | if (!entity) return
175 | trigger('beforeUnmount', entity, [entity.context, entity.nativeElement])
176 | unmountChildren(entityId)
177 | removeAllEvents(entityId)
178 | var componentEntities = components[entityId].entities
179 | delete componentEntities[entityId]
180 | delete components[entityId]
181 | delete entities[entityId]
182 | delete children[entityId]
183 | }
184 |
185 | /**
186 | * Render the entity and make sure it returns a node
187 | *
188 | * @param {Entity} entity
189 | *
190 | * @return {VirtualTree}
191 | */
192 |
193 | function renderEntity (entity) {
194 | var component = entity.component
195 | var fn = typeof component === 'function' ? component : component.render
196 | if (!fn) throw new Error('Component needs a render function')
197 | var result = fn(entity.context, setState(entity))
198 | if (!result) throw new Error('Render function must return an element.')
199 | return result
200 | }
201 |
202 | /**
203 | * Whenever setState or setProps is called, we mark the entity
204 | * as dirty in the renderer. This lets us optimize the re-rendering
205 | * and skip components that definitely haven't changed.
206 | *
207 | * @param {Entity} entity
208 | *
209 | * @return {Function} A curried function for updating the state of an entity
210 | */
211 |
212 | function setState (entity) {
213 | return function (nextState) {
214 | updateEntityState(entity, nextState)
215 | }
216 | }
217 |
218 | /**
219 | * Tell the app it's dirty and needs to re-render. If batching is disabled
220 | * we can just trigger a render immediately, otherwise we'll wait until
221 | * the next available frame.
222 | */
223 |
224 | function invalidate () {
225 | if (!options.batching) {
226 | if (!isRendering) render()
227 | } else {
228 | if (!frameId) frameId = raf(render)
229 | }
230 | }
231 |
232 | /**
233 | * Update the DOM. If the update fails we stop the loop
234 | * so we don't get errors on every frame.
235 | *
236 | * @api public
237 | */
238 |
239 | function render () {
240 | // If this is called synchronously we need to
241 | // cancel any pending future updates
242 | clearFrame()
243 |
244 | // If the rendering from the previous frame is still going,
245 | // we'll just wait until the next frame. Ideally renders should
246 | // not take over 16ms to stay within a single frame, but this should
247 | // catch it if it does.
248 | if (isRendering) {
249 | frameId = raf(render)
250 | return
251 | } else {
252 | isRendering = true
253 | }
254 |
255 | // 1. If there isn't a native element rendered for the current mounted element
256 | // then we need to create it from scratch.
257 | // 2. If a new element has been mounted, we should diff them.
258 | // 3. We should update check all child components for changes.
259 | if (!currentNativeElement) {
260 | currentElement = app.element
261 | currentNativeElement = toNative(rootId, '0', currentElement)
262 | if (container.children.length > 0) {
263 | console.info('deku: The container element is not empty. These elements will be removed. Read more: http://cl.ly/b0Sr')
264 | }
265 | if (container === document.body) {
266 | console.warn('deku: Using document.body is allowed but it can cause some issues. Read more: http://cl.ly/b0SC')
267 | }
268 | removeAllChildren(container)
269 | container.appendChild(currentNativeElement)
270 | } else if (currentElement !== app.element) {
271 | currentNativeElement = patch(rootId, currentElement, app.element, currentNativeElement)
272 | currentElement = app.element
273 | updateChildren(rootId)
274 | } else {
275 | updateChildren(rootId)
276 | }
277 |
278 | // Call mount events on all new entities
279 | flushMountQueue()
280 |
281 | // Allow rendering again.
282 | isRendering = false
283 | }
284 |
285 | /**
286 | * Call hooks for all new entities that have been created in
287 | * the last render from the bottom up.
288 | */
289 |
290 | function flushMountQueue () {
291 | while (mountQueue.length > 0) {
292 | var entityId = mountQueue.shift()
293 | var entity = entities[entityId]
294 | trigger('afterRender', entity, [entity.context, entity.nativeElement])
295 | trigger('afterMount', entity, [entity.context, entity.nativeElement, setState(entity)])
296 | }
297 | }
298 |
299 | /**
300 | * Clear the current scheduled frame
301 | */
302 |
303 | function clearFrame () {
304 | if (!frameId) return
305 | raf.cancel(frameId)
306 | frameId = 0
307 | }
308 |
309 | /**
310 | * Update a component.
311 | *
312 | * The entity is just the data object for a component instance.
313 | *
314 | * @param {String} id Component instance id.
315 | */
316 |
317 | function updateEntity (entityId) {
318 | var entity = entities[entityId]
319 | setSources(entity)
320 |
321 | if (!shouldUpdate(entity)) {
322 | commit(entity)
323 | return updateChildren(entityId)
324 | }
325 |
326 | var currentTree = entity.virtualElement
327 | var nextProps = entity.pendingProps
328 | var nextState = entity.pendingState
329 | var previousState = entity.context.state
330 | var previousProps = entity.context.props
331 |
332 | // hook before rendering. could modify state just before the render occurs.
333 | trigger('beforeUpdate', entity, [entity.context, nextProps, nextState])
334 | trigger('beforeRender', entity, [entity.context])
335 |
336 | // commit state and props.
337 | commit(entity)
338 |
339 | // re-render.
340 | var nextTree = renderEntity(entity)
341 |
342 | // if the tree is the same we can just skip this component
343 | // but we should still check the children to see if they're dirty.
344 | // This allows us to memoize the render function of components.
345 | if (nextTree === currentTree) return updateChildren(entityId)
346 |
347 | // apply new virtual tree to native dom.
348 | entity.nativeElement = patch(entityId, currentTree, nextTree, entity.nativeElement)
349 | entity.virtualElement = nextTree
350 | updateChildren(entityId)
351 |
352 | // trigger render hook
353 | trigger('afterRender', entity, [entity.context, entity.nativeElement])
354 |
355 | // trigger afterUpdate after all children have updated.
356 | trigger('afterUpdate', entity, [entity.context, previousProps, previousState, setState(entity)])
357 | }
358 |
359 | /**
360 | * Update all the children of an entity.
361 | *
362 | * @param {String} id Component instance id.
363 | */
364 |
365 | function updateChildren (entityId) {
366 | forEach(children[entityId], function (childId) {
367 | updateEntity(childId)
368 | })
369 | }
370 |
371 | /**
372 | * Remove all of the child entities of an entity
373 | *
374 | * @param {Entity} entity
375 | */
376 |
377 | function unmountChildren (entityId) {
378 | forEach(children[entityId], function (childId) {
379 | unmountEntity(childId)
380 | })
381 | }
382 |
383 | /**
384 | * Remove the root element. If this is called synchronously we need to
385 | * cancel any pending future updates.
386 | */
387 |
388 | function removeNativeElement () {
389 | clearFrame()
390 | removeElement(rootId, '0', currentNativeElement)
391 | currentNativeElement = null
392 | }
393 |
394 | /**
395 | * Create a native element from a virtual element.
396 | *
397 | * @param {String} entityId
398 | * @param {String} path
399 | * @param {Object} vnode
400 | *
401 | * @return {HTMLDocumentFragment}
402 | */
403 |
404 | function toNative (entityId, path, vnode) {
405 | switch (nodeType(vnode)) {
406 | case 'text': return toNativeText(vnode)
407 | case 'empty': return toNativeEmptyElement(entityId, path)
408 | case 'element': return toNativeElement(entityId, path, vnode)
409 | case 'component': return toNativeComponent(entityId, path, vnode)
410 | }
411 | }
412 |
413 | /**
414 | * Create a native text element from a virtual element.
415 | *
416 | * @param {Object} vnode
417 | */
418 |
419 | function toNativeText (text) {
420 | return document.createTextNode(text)
421 | }
422 |
423 | /**
424 | * Create a native element from a virtual element.
425 | */
426 |
427 | function toNativeElement (entityId, path, vnode) {
428 | var el
429 | var attributes = vnode.attributes
430 | var tagName = vnode.type
431 | var childNodes = vnode.children
432 |
433 | // create element either from pool or fresh.
434 | if (svg.isElement(tagName)) {
435 | el = document.createElementNS(svg.namespace, tagName)
436 | } else {
437 | el = document.createElement(tagName)
438 | }
439 |
440 | // set attributes.
441 | forEach(attributes, function (value, name) {
442 | setAttribute(entityId, path, el, name, value)
443 | })
444 |
445 | // add children.
446 | forEach(childNodes, function (child, i) {
447 | var childEl = toNative(entityId, path + '.' + i, child)
448 | if (!childEl.parentNode) el.appendChild(childEl)
449 | })
450 |
451 | // store keys on the native element for fast event handling.
452 | el.__entity__ = entityId
453 | el.__path__ = path
454 |
455 | return el
456 | }
457 |
458 | /**
459 | * Create a native element from a virtual element.
460 | */
461 |
462 | function toNativeEmptyElement (entityId, path) {
463 | var el = document.createElement('noscript')
464 | el.__entity__ = entityId
465 | el.__path__ = path
466 | return el
467 | }
468 |
469 | /**
470 | * Create a native element from a component.
471 | */
472 |
473 | function toNativeComponent (entityId, path, vnode) {
474 | var child = new Entity(vnode.type, assign({ children: vnode.children }, vnode.attributes), entityId)
475 | children[entityId][path] = child.id
476 | return mountEntity(child)
477 | }
478 |
479 | /**
480 | * Patch an element with the diff from two trees.
481 | */
482 |
483 | function patch (entityId, prev, next, el) {
484 | return diffNode('0', entityId, prev, next, el)
485 | }
486 |
487 | /**
488 | * Create a diff between two trees of nodes.
489 | */
490 |
491 | function diffNode (path, entityId, prev, next, el) {
492 | var leftType = nodeType(prev)
493 | var rightType = nodeType(next)
494 |
495 | // Type changed. This could be from element->text, text->ComponentA,
496 | // ComponentA->ComponentB etc. But NOT div->span. These are the same type
497 | // (ElementNode) but different tag name.
498 | if (leftType !== rightType) return replaceElement(entityId, path, el, next)
499 |
500 | switch (rightType) {
501 | case 'text': return diffText(prev, next, el)
502 | case 'empty': return el
503 | case 'element': return diffElement(path, entityId, prev, next, el)
504 | case 'component': return diffComponent(path, entityId, prev, next, el)
505 | }
506 | }
507 |
508 | /**
509 | * Diff two text nodes and update the element.
510 | */
511 |
512 | function diffText (previous, current, el) {
513 | if (current !== previous) el.data = current
514 | return el
515 | }
516 |
517 | /**
518 | * Diff the children of an ElementNode.
519 | */
520 |
521 | function diffChildren (path, entityId, prev, next, el) {
522 | var positions = []
523 | var hasKeys = false
524 | var childNodes = Array.prototype.slice.apply(el.childNodes)
525 | var leftKeys = reduce(prev.children, keyMapReducer, {})
526 | var rightKeys = reduce(next.children, keyMapReducer, {})
527 | var currentChildren = assign({}, children[entityId])
528 |
529 | function keyMapReducer (acc, child, i) {
530 | if (child && child.attributes && child.attributes.key != null) {
531 | acc[child.attributes.key] = {
532 | element: child,
533 | index: i
534 | }
535 | hasKeys = true
536 | }
537 | return acc
538 | }
539 |
540 | // Diff all of the nodes that have keys. This lets us re-used elements
541 | // instead of overriding them and lets us move them around.
542 | if (hasKeys) {
543 | // Removals
544 | forEach(leftKeys, function (leftNode, key) {
545 | if (rightKeys[key] == null) {
546 | var leftPath = path + '.' + leftNode.index
547 | removeElement(
548 | entityId,
549 | leftPath,
550 | childNodes[leftNode.index]
551 | )
552 | }
553 | })
554 |
555 | // Update nodes
556 | forEach(rightKeys, function (rightNode, key) {
557 | var leftNode = leftKeys[key]
558 |
559 | // We only want updates for now
560 | if (leftNode == null) return
561 |
562 | var leftPath = path + '.' + leftNode.index
563 |
564 | // Updated
565 | positions[rightNode.index] = diffNode(
566 | leftPath,
567 | entityId,
568 | leftNode.element,
569 | rightNode.element,
570 | childNodes[leftNode.index]
571 | )
572 | })
573 |
574 | // Update the positions of all child components and event handlers
575 | forEach(rightKeys, function (rightNode, key) {
576 | var leftNode = leftKeys[key]
577 |
578 | // We just want elements that have moved around
579 | if (leftNode == null || leftNode.index === rightNode.index) return
580 |
581 | var rightPath = path + '.' + rightNode.index
582 | var leftPath = path + '.' + leftNode.index
583 |
584 | // Update all the child component path positions to match
585 | // the latest positions if they've changed. This is a bit hacky.
586 | forEach(currentChildren, function (childId, childPath) {
587 | if (leftPath === childPath) {
588 | delete children[entityId][childPath]
589 | children[entityId][rightPath] = childId
590 | }
591 | })
592 | })
593 |
594 | // Now add all of the new nodes last in case their path
595 | // would have conflicted with one of the previous paths.
596 | forEach(rightKeys, function (rightNode, key) {
597 | var rightPath = path + '.' + rightNode.index
598 | if (leftKeys[key] == null) {
599 | positions[rightNode.index] = toNative(
600 | entityId,
601 | rightPath,
602 | rightNode.element
603 | )
604 | }
605 | })
606 | } else {
607 | var maxLength = Math.max(prev.children.length, next.children.length)
608 | // Now diff all of the nodes that don't have keys
609 | for (var i = 0; i < maxLength; i++) {
610 | var leftNode = prev.children[i]
611 | var rightNode = next.children[i]
612 |
613 | // Removals
614 | if (rightNode === undefined) {
615 | removeElement(
616 | entityId,
617 | path + '.' + i,
618 | childNodes[i]
619 | )
620 | continue
621 | }
622 |
623 | // New Node
624 | if (leftNode === undefined) {
625 | positions[i] = toNative(
626 | entityId,
627 | path + '.' + i,
628 | rightNode
629 | )
630 | continue
631 | }
632 |
633 | // Updated
634 | positions[i] = diffNode(
635 | path + '.' + i,
636 | entityId,
637 | leftNode,
638 | rightNode,
639 | childNodes[i]
640 | )
641 | }
642 | }
643 |
644 | // Reposition all the elements
645 | forEach(positions, function (childEl, newPosition) {
646 | var target = el.childNodes[newPosition]
647 | if (childEl && childEl !== target) {
648 | if (target) {
649 | el.insertBefore(childEl, target)
650 | } else {
651 | el.appendChild(childEl)
652 | }
653 | }
654 | })
655 | }
656 |
657 | /**
658 | * Diff the attributes and add/remove them.
659 | */
660 |
661 | function diffAttributes (prev, next, el, entityId, path) {
662 | var nextAttrs = next.attributes
663 | var prevAttrs = prev.attributes
664 |
665 | // add new attrs
666 | forEach(nextAttrs, function (value, name) {
667 | if (events[name] || !(name in prevAttrs) || prevAttrs[name] !== value) {
668 | setAttribute(entityId, path, el, name, value)
669 | }
670 | })
671 |
672 | // remove old attrs
673 | forEach(prevAttrs, function (value, name) {
674 | if (!(name in nextAttrs)) {
675 | removeAttribute(entityId, path, el, name)
676 | }
677 | })
678 | }
679 |
680 | /**
681 | * Update a component with the props from the next node. If
682 | * the component type has changed, we'll just remove the old one
683 | * and replace it with the new component.
684 | */
685 |
686 | function diffComponent (path, entityId, prev, next, el) {
687 | if (next.type !== prev.type) {
688 | return replaceElement(entityId, path, el, next)
689 | } else {
690 | var targetId = children[entityId][path]
691 |
692 | // This is a hack for now
693 | if (targetId) {
694 | updateEntityProps(targetId, assign({ children: next.children }, next.attributes))
695 | }
696 |
697 | return el
698 | }
699 | }
700 |
701 | /**
702 | * Diff two element nodes.
703 | */
704 |
705 | function diffElement (path, entityId, prev, next, el) {
706 | if (next.type !== prev.type) return replaceElement(entityId, path, el, next)
707 | diffAttributes(prev, next, el, entityId, path)
708 | diffChildren(path, entityId, prev, next, el)
709 | return el
710 | }
711 |
712 | /**
713 | * Removes an element from the DOM and unmounts and components
714 | * that are within that branch
715 | *
716 | * side effects:
717 | * - removes element from the DOM
718 | * - removes internal references
719 | *
720 | * @param {String} entityId
721 | * @param {String} path
722 | * @param {HTMLElement} el
723 | */
724 |
725 | function removeElement (entityId, path, el) {
726 | var childrenByPath = children[entityId]
727 | var childId = childrenByPath[path]
728 | var entityHandlers = handlers[entityId] || {}
729 | var removals = []
730 |
731 | // If the path points to a component we should use that
732 | // components element instead, because it might have moved it.
733 | if (childId) {
734 | var child = entities[childId]
735 | el = child.nativeElement
736 | unmountEntity(childId)
737 | removals.push(path)
738 | } else {
739 | // Just remove the text node
740 | if (!isElement(el)) return el && el.parentNode.removeChild(el)
741 | // Then we need to find any components within this
742 | // branch and unmount them.
743 | forEach(childrenByPath, function (childId, childPath) {
744 | if (childPath === path || isWithinPath(path, childPath)) {
745 | unmountEntity(childId)
746 | removals.push(childPath)
747 | }
748 | })
749 |
750 | // Remove all events at this path or below it
751 | forEach(entityHandlers, function (fn, handlerPath) {
752 | if (handlerPath === path || isWithinPath(path, handlerPath)) {
753 | removeEvent(entityId, handlerPath)
754 | }
755 | })
756 | }
757 |
758 | // Remove the paths from the object without touching the
759 | // old object. This keeps the object using fast properties.
760 | forEach(removals, function (path) {
761 | delete children[entityId][path]
762 | })
763 |
764 | // Remove it from the DOM
765 | el.parentNode.removeChild(el)
766 | }
767 |
768 | /**
769 | * Replace an element in the DOM. Removing all components
770 | * within that element and re-rendering the new virtual node.
771 | *
772 | * @param {Entity} entity
773 | * @param {String} path
774 | * @param {HTMLElement} el
775 | * @param {Object} vnode
776 | *
777 | * @return {void}
778 | */
779 |
780 | function replaceElement (entityId, path, el, vnode) {
781 | var parent = el.parentNode
782 | var index = Array.prototype.indexOf.call(parent.childNodes, el)
783 |
784 | // remove the previous element and all nested components. This
785 | // needs to happen before we create the new element so we don't
786 | // get clashes on the component paths.
787 | removeElement(entityId, path, el)
788 |
789 | // then add the new element in there
790 | var newEl = toNative(entityId, path, vnode)
791 | var target = parent.childNodes[index]
792 |
793 | if (target) {
794 | parent.insertBefore(newEl, target)
795 | } else {
796 | parent.appendChild(newEl)
797 | }
798 |
799 | // walk up the tree and update all `entity.nativeElement` references.
800 | if (entityId !== 'root' && path === '0') {
801 | updateNativeElement(entityId, newEl)
802 | }
803 |
804 | return newEl
805 | }
806 |
807 | /**
808 | * Update all entities in a branch that have the same nativeElement. This
809 | * happens when a component has another component as it's root node.
810 | *
811 | * @param {String} entityId
812 | * @param {HTMLElement} newEl
813 | *
814 | * @return {void}
815 | */
816 |
817 | function updateNativeElement (entityId, newEl) {
818 | var target = entities[entityId]
819 | if (target.ownerId === 'root') return
820 | if (children[target.ownerId]['0'] === entityId) {
821 | entities[target.ownerId].nativeElement = newEl
822 | updateNativeElement(target.ownerId, newEl)
823 | }
824 | }
825 |
826 | /**
827 | * Set the attribute of an element, performing additional transformations
828 | * dependning on the attribute name
829 | *
830 | * @param {HTMLElement} el
831 | * @param {String} name
832 | * @param {String} value
833 | */
834 |
835 | function setAttribute (entityId, path, el, name, value) {
836 | if (!value && typeof value !== 'number') {
837 | removeAttribute(entityId, path, el, name)
838 | return
839 | }
840 | if (events[name]) {
841 | addEvent(entityId, path, events[name], value)
842 | return
843 | }
844 | switch (name) {
845 | case 'checked':
846 | case 'disabled':
847 | case 'selected':
848 | el[name] = true
849 | break
850 | case 'innerHTML':
851 | el.innerHTML = value
852 | break
853 | case 'value':
854 | setElementValue(el, value)
855 | break
856 | case svg.isAttribute(name):
857 | el.setAttributeNS(svg.namespace, name, value)
858 | break
859 | default:
860 | el.setAttribute(name, value)
861 | break
862 | }
863 | }
864 |
865 | /**
866 | * Remove an attribute, performing additional transformations
867 | * dependning on the attribute name
868 | *
869 | * @param {HTMLElement} el
870 | * @param {String} name
871 | */
872 |
873 | function removeAttribute (entityId, path, el, name) {
874 | if (events[name]) {
875 | removeEvent(entityId, path, events[name])
876 | return
877 | }
878 | switch (name) {
879 | case 'checked':
880 | case 'disabled':
881 | case 'selected':
882 | el[name] = false
883 | break
884 | case 'innerHTML':
885 | el.innerHTML = ''
886 | /* falls through */
887 | case 'value':
888 | setElementValue(el, null)
889 | break
890 | default:
891 | el.removeAttribute(name)
892 | break
893 | }
894 | }
895 |
896 | /**
897 | * Checks to see if one tree path is within
898 | * another tree path. Example:
899 | *
900 | * 0.1 vs 0.1.1 = true
901 | * 0.2 vs 0.3.5 = false
902 | *
903 | * @param {String} target
904 | * @param {String} path
905 | *
906 | * @return {Boolean}
907 | */
908 |
909 | function isWithinPath (target, path) {
910 | return path.indexOf(target + '.') === 0
911 | }
912 |
913 | /**
914 | * Is the DOM node an element node
915 | *
916 | * @param {HTMLElement} el
917 | *
918 | * @return {Boolean}
919 | */
920 |
921 | function isElement (el) {
922 | return !!(el && el.tagName)
923 | }
924 |
925 | /**
926 | * Remove all the child nodes from an element
927 | *
928 | * @param {HTMLElement} el
929 | */
930 |
931 | function removeAllChildren (el) {
932 | while (el.firstChild) el.removeChild(el.firstChild)
933 | }
934 |
935 | /**
936 | * Trigger a hook on a component.
937 | *
938 | * @param {String} name Name of hook.
939 | * @param {Entity} entity The component instance.
940 | * @param {Array} args To pass along to hook.
941 | */
942 |
943 | function trigger (name, entity, args) {
944 | if (typeof entity.component[name] !== 'function') return
945 | return entity.component[name].apply(null, args)
946 | }
947 |
948 | /**
949 | * Update an entity to match the latest rendered vode. We always
950 | * replace the props on the component when composing them. This
951 | * will trigger a re-render on all children below this point.
952 | *
953 | * @param {Entity} entity
954 | * @param {String} path
955 | * @param {Object} vnode
956 | *
957 | * @return {void}
958 | */
959 |
960 | function updateEntityProps (entityId, nextProps) {
961 | var entity = entities[entityId]
962 | entity.pendingProps = defaults({}, nextProps, entity.component.defaultProps || {})
963 | entity.dirty = true
964 | invalidate()
965 | }
966 |
967 | /**
968 | * Update component instance state.
969 | */
970 |
971 | function updateEntityState (entity, nextState) {
972 | entity.pendingState = assign(entity.pendingState, nextState)
973 | entity.dirty = true
974 | invalidate()
975 | }
976 |
977 | /**
978 | * Commit props and state changes to an entity.
979 | */
980 |
981 | function commit (entity) {
982 | entity.context = {
983 | state: entity.pendingState,
984 | props: entity.pendingProps,
985 | id: entity.id
986 | }
987 | entity.pendingState = assign({}, entity.context.state)
988 | entity.pendingProps = assign({}, entity.context.props)
989 | entity.dirty = false
990 | if (typeof entity.component.validate === 'function') {
991 | entity.component.validate(entity.context)
992 | }
993 | }
994 |
995 | /**
996 | * Try to avoid creating new virtual dom if possible.
997 | *
998 | * Later we may expose this so you can override, but not there yet.
999 | */
1000 |
1001 | function shouldUpdate (entity) {
1002 | if (!entity.dirty) return false
1003 | if (!entity.component.shouldUpdate) return true
1004 | var nextProps = entity.pendingProps
1005 | var nextState = entity.pendingState
1006 | var bool = entity.component.shouldUpdate(entity.context, nextProps, nextState)
1007 | return bool
1008 | }
1009 |
1010 | /**
1011 | * Register an entity.
1012 | *
1013 | * This is mostly to pre-preprocess component properties and values chains.
1014 | *
1015 | * The end result is for every component that gets mounted,
1016 | * you create a set of IO nodes in the network from the `value` definitions.
1017 | *
1018 | * @param {Component} component
1019 | */
1020 |
1021 | function register (entity) {
1022 | registerEntity(entity)
1023 | var component = entity.component
1024 | if (component.registered) return
1025 |
1026 | // initialize sources once for a component type.
1027 | registerSources(entity)
1028 | component.registered = true
1029 | }
1030 |
1031 | /**
1032 | * Add entity to data-structures related to components/entities.
1033 | *
1034 | * @param {Entity} entity
1035 | */
1036 |
1037 | function registerEntity (entity) {
1038 | var component = entity.component
1039 | // all entities for this component type.
1040 | var entities = component.entities = component.entities || {}
1041 | // add entity to component list
1042 | entities[entity.id] = entity
1043 | // map to component so you can remove later.
1044 | components[entity.id] = component
1045 | }
1046 |
1047 | /**
1048 | * Initialize sources for a component by type.
1049 | *
1050 | * @param {Entity} entity
1051 | */
1052 |
1053 | function registerSources (entity) {
1054 | var component = components[entity.id]
1055 | // get 'class-level' sources.
1056 | // if we've already hooked it up, then we're good.
1057 | var sources = component.sources
1058 | if (sources) return
1059 | var entities = component.entities
1060 |
1061 | // hook up sources.
1062 | var map = component.sourceToPropertyName = {}
1063 | component.sources = sources = []
1064 | var propTypes = component.propTypes
1065 | for (var name in propTypes) {
1066 | var data = propTypes[name]
1067 | if (!data) continue
1068 | if (!data.source) continue
1069 | sources.push(data.source)
1070 | map[data.source] = name
1071 | }
1072 |
1073 | // send value updates to all component instances.
1074 | sources.forEach(function (source) {
1075 | connections[source] = connections[source] || []
1076 | connections[source].push(update)
1077 |
1078 | function update (data) {
1079 | var prop = map[source]
1080 | for (var entityId in entities) {
1081 | var entity = entities[entityId]
1082 | var changes = {}
1083 | changes[prop] = data
1084 | updateEntityProps(entityId, assign(entity.pendingProps, changes))
1085 | }
1086 | }
1087 | })
1088 | }
1089 |
1090 | /**
1091 | * Set the initial source value on the entity
1092 | *
1093 | * @param {Entity} entity
1094 | */
1095 |
1096 | function setSources (entity) {
1097 | var component = entity.component
1098 | var map = component.sourceToPropertyName
1099 | var sources = component.sources
1100 | sources.forEach(function (source) {
1101 | var name = map[source]
1102 | if (entity.pendingProps[name] != null) return
1103 | entity.pendingProps[name] = app.sources[source] // get latest value plugged into global store
1104 | })
1105 | }
1106 |
1107 | /**
1108 | * Add all of the DOM event listeners
1109 | */
1110 |
1111 | function addNativeEventListeners () {
1112 | forEach(events, function (eventType) {
1113 | rootElement.addEventListener(eventType, handleEvent, true)
1114 | })
1115 | }
1116 |
1117 | /**
1118 | * Add all of the DOM event listeners
1119 | */
1120 |
1121 | function removeNativeEventListeners () {
1122 | forEach(events, function (eventType) {
1123 | rootElement.removeEventListener(eventType, handleEvent, true)
1124 | })
1125 | }
1126 |
1127 | /**
1128 | * Handle an event that has occured within the container
1129 | *
1130 | * @param {Event} event
1131 | */
1132 |
1133 | function handleEvent (event) {
1134 | var target = event.target
1135 | var eventType = event.type
1136 |
1137 | // Walk up the DOM tree and see if there is a handler
1138 | // for this event type higher up.
1139 | while (target) {
1140 | var fn = keypath.get(handlers, [target.__entity__, target.__path__, eventType])
1141 | if (fn) {
1142 | event.delegateTarget = target
1143 | if (fn(event) === false) break
1144 | }
1145 | target = target.parentNode
1146 | }
1147 | }
1148 |
1149 | /**
1150 | * Bind events for an element, and all it's rendered child elements.
1151 | *
1152 | * @param {String} path
1153 | * @param {String} event
1154 | * @param {Function} fn
1155 | */
1156 |
1157 | function addEvent (entityId, path, eventType, fn) {
1158 | keypath.set(handlers, [entityId, path, eventType], function (e) {
1159 | var entity = entities[entityId]
1160 | if (entity) {
1161 | return fn(e, entity.context, setState(entity))
1162 | } else {
1163 | return fn(e)
1164 | }
1165 | })
1166 | }
1167 |
1168 | /**
1169 | * Unbind events for a entityId
1170 | *
1171 | * @param {String} entityId
1172 | */
1173 |
1174 | function removeEvent (entityId, path, eventType) {
1175 | var args = [entityId]
1176 | if (path) args.push(path)
1177 | if (eventType) args.push(eventType)
1178 | keypath.del(handlers, args)
1179 | }
1180 |
1181 | /**
1182 | * Unbind all events from an entity
1183 | *
1184 | * @param {Entity} entity
1185 | */
1186 |
1187 | function removeAllEvents (entityId) {
1188 | keypath.del(handlers, [entityId])
1189 | }
1190 |
1191 | /**
1192 | * Used for debugging to inspect the current state without
1193 | * us needing to explicitly manage storing/updating references.
1194 | *
1195 | * @return {Object}
1196 | */
1197 |
1198 | function inspect () {
1199 | return {
1200 | entities: entities,
1201 | handlers: handlers,
1202 | connections: connections,
1203 | currentElement: currentElement,
1204 | options: options,
1205 | app: app,
1206 | container: container,
1207 | children: children
1208 | }
1209 | }
1210 |
1211 | /**
1212 | * Return an object that lets us completely remove the automatic
1213 | * DOM rendering and export debugging tools.
1214 | */
1215 |
1216 | return {
1217 | remove: teardown,
1218 | inspect: inspect
1219 | }
1220 | }
1221 |
1222 | /**
1223 | * A rendered component instance.
1224 | *
1225 | * This manages the lifecycle, props and state of the component.
1226 | * It's basically just a data object for more straightfoward lookup.
1227 | *
1228 | * @param {Component} component
1229 | * @param {Object} props
1230 | */
1231 |
1232 | function Entity (component, props, ownerId) {
1233 | this.id = uid()
1234 | this.ownerId = ownerId
1235 | this.component = component
1236 | this.propTypes = component.propTypes || {}
1237 | this.context = {}
1238 | this.context.id = this.id
1239 | this.context.props = defaults(props || {}, component.defaultProps || {})
1240 | this.context.state = this.component.initialState ? this.component.initialState(this.context.props) : {}
1241 | this.pendingProps = assign({}, this.context.props)
1242 | this.pendingState = assign({}, this.context.state)
1243 | this.dirty = false
1244 | this.virtualElement = null
1245 | this.nativeElement = null
1246 | this.displayName = component.name || 'Component'
1247 | }
1248 |
1249 | /**
1250 | * Retrieve the nearest 'body' ancestor of the given element or else the root
1251 | * element of the document in which stands the given element.
1252 | *
1253 | * This is necessary if you want to attach the events handler to the correct
1254 | * element and be able to dispatch events in document fragments such as
1255 | * Shadow DOM.
1256 | *
1257 | * @param {HTMLElement} el The element on which we will render an app.
1258 | * @return {HTMLElement} The root element on which we will attach the events
1259 | * handler.
1260 | */
1261 |
1262 | function getRootElement (el) {
1263 | while (el.parentElement) {
1264 | if (el.tagName === 'BODY' || !el.parentElement) {
1265 | return el
1266 | }
1267 | el = el.parentElement
1268 | }
1269 | return el
1270 | }
1271 |
1272 | /**
1273 | * Set the value property of an element and keep the text selection
1274 | * for input fields.
1275 | *
1276 | * @param {HTMLElement} el
1277 | * @param {String} value
1278 | */
1279 |
1280 | function setElementValue (el, value) {
1281 | if (el === document.activeElement && canSelectText(el)) {
1282 | var start = el.selectionStart
1283 | var end = el.selectionEnd
1284 | el.value = value
1285 | el.setSelectionRange(start, end)
1286 | } else {
1287 | el.value = value
1288 | }
1289 | }
1290 |
1291 | /**
1292 | * For some reason only certain types of inputs can set the selection range.
1293 | *
1294 | * @param {HTMLElement} el
1295 | *
1296 | * @return {Boolean}
1297 | */
1298 |
1299 | function canSelectText (el) {
1300 | return el.tagName === 'INPUT' && ['text', 'search', 'password', 'tel', 'url'].indexOf(el.type) > -1
1301 | }
1302 |
--------------------------------------------------------------------------------
/test/dom/index.js:
--------------------------------------------------------------------------------
1 | /** @jsx dom */
2 |
3 | import trigger from 'trigger-event'
4 | import Emitter from 'component-emitter'
5 | import raf from 'component-raf'
6 | import {deku,render} from '../../'
7 | import dom from 'virtual-element'
8 | import test from 'tape'
9 |
10 | // Test Components
11 |
12 | var RenderChildren = ({props}) => props.children[0]
13 | var ListItem = ({props}) => {props.children}
14 | var Wrapper = ({props}) => {props.children}
15 | var TwoWords = ({props}) => {props.one} {props.two}
16 |
17 | var StateChangeOnMount = {
18 | initialState: p => ({text: 'foo'}),
19 | afterMount: (c,el,setState) => setState({ text: 'bar' }),
20 | render: ({state}) => {state.text}
21 | }
22 |
23 | var Delegate = function ({props,state}) {
24 | var active = state.active || 0
25 | var items = [1,2,3].map(i => {
26 | setState({ active: i })}>
27 | link
28 |
29 | })
30 | return
31 | }
32 |
33 | // Test helpers
34 |
35 | var div = function(){
36 | var el = document.createElement('div')
37 | document.body.appendChild(el)
38 | return el
39 | }
40 |
41 | var setup = function (equal) {
42 | var app = deku()
43 | var el = div()
44 | var renderer = render(app, el, { batching: false })
45 | var $ = el.querySelector.bind(el)
46 | var mount = app.mount.bind(app)
47 | var unmount = app.unmount.bind(app)
48 | var html = createAssertHTML(el, equal)
49 | return {renderer, el, app, $, mount, unmount, html}
50 | }
51 |
52 | var teardown = function ({ renderer, el }) {
53 | renderer.remove()
54 | if (el.parentNode) el.parentNode.removeChild(el)
55 | }
56 |
57 | var createAssertHTML = function(container, equal) {
58 | var dummy = document.createElement('div')
59 | return function (html, message) {
60 | html = html.replace(/\n(\s+)?/g,'').replace(/\s+/g,' ')
61 | equal(html, container.innerHTML, message || 'innerHTML is equal')
62 | }
63 | }
64 |
65 | // Tests
66 |
67 | test('rendering DOM', ({equal,end,notEqual,pass,fail}) => {
68 | var {renderer,el,mount,unmount,html} = setup(equal)
69 | var rootEl
70 |
71 | // Render
72 | mount( )
73 | html(' ', 'no attribute')
74 |
75 | // Add
76 | mount( )
77 | html(' ', 'attribute added')
78 |
79 | // Update
80 | mount( )
81 | html(' ', 'attribute updated')
82 |
83 | // Update
84 | mount( )
85 | html(' ', 'attribute removed with null')
86 |
87 | // Update
88 | mount( )
89 | html(' ', 'attribute removed with undefined')
90 |
91 | // Update
92 | mount( )
93 | el.children[0].setAttribute = () => fail('DOM was touched')
94 |
95 | // Update
96 | mount( )
97 | pass('DOM not updated without change')
98 |
99 | // Update
100 | mount(Hello World )
101 | html(`Hello World `, 'text rendered')
102 |
103 | rootEl = el.firstChild
104 |
105 | // Update
106 | mount(Hello Pluto )
107 | html('Hello Pluto ', 'text updated')
108 |
109 | // Remove
110 | mount( )
111 | html(' ', 'text removed')
112 |
113 | // Update
114 | mount({undefined} World )
115 | html(' World ', 'text was replaced by undefined')
116 |
117 | // Root element should still be the same
118 | equal(el.firstChild, rootEl, 'root element not replaced')
119 |
120 | // Replace
121 | mount(Foo!
)
122 | html('Foo!
', 'element is replaced')
123 | notEqual(el.firstChild, rootEl, 'root element replaced')
124 |
125 | // Clear
126 | unmount()
127 | html('', 'element is removed when unmounted')
128 |
129 | // Render
130 | mount(Foo!
)
131 | html('Foo!
', 'element is rendered again')
132 |
133 | rootEl = el.firstChild
134 |
135 | // Update
136 | mount(
)
137 | html('
', 'replaced text with an element')
138 |
139 | // Update
140 | mount(bar
)
141 | html('bar
', 'replaced child with text')
142 |
143 | // Update
144 | mount(Hello World
)
145 | html('Hello World
', 'replaced text with element')
146 |
147 | // Remove
148 | mount(
)
149 | html('
', 'removed element')
150 | equal(el.firstChild, rootEl, 'root element not replaced')
151 |
152 | // Children added
153 | mount(
154 |
155 | one
156 | two
157 | three
158 |
159 | )
160 | html(`
161 |
162 | one
163 | two
164 | three
165 |
`
166 | )
167 | equal(el.firstChild, rootEl, 'root element not replaced')
168 | var span = el.firstChild.firstChild
169 |
170 | // Siblings removed
171 | mount(
172 |
173 | one
174 |
175 | )
176 | html('one
', 'added element')
177 | equal(el.firstChild.firstChild, span, 'child element not replaced')
178 | equal(el.firstChild, rootEl, 'root element not replaced')
179 |
180 | // Removing the renderer
181 | teardown({ renderer, el })
182 | html('', 'element is removed')
183 | end()
184 | })
185 |
186 | test('falsy attributes should not touch the DOM', ({equal,end,pass,fail}) => {
187 | var {renderer,el,mount} = setup(equal)
188 | mount( )
189 | var child = el.children[0]
190 | child.setAttribute = () => fail('should not set attributes')
191 | child.removeAttribute = () => fail('should not remove attributes')
192 | mount( )
193 | pass('DOM not touched')
194 | teardown({ renderer, el })
195 | end()
196 | })
197 |
198 | test('innerHTML attribute', ({equal,end}) => {
199 | var {html,mount,el,renderer} = setup(equal)
200 | mount(
)
201 | html('Hello deku
', 'innerHTML is rendered')
202 | mount(
)
203 | html('Hello Pluto
', 'innerHTML is updated')
204 | mount(
)
205 | // Causing issues in IE10. Renders with a for some reason
206 | // html('
', 'innerHTML is removed')
207 | teardown({renderer,el})
208 | end()
209 | })
210 |
211 | test('input attributes', ({equal,notEqual,end,ok,test,comment}) => {
212 | var {html,mount,el,renderer,$} = setup(equal)
213 | mount( )
214 | var checkbox = $('input')
215 |
216 | comment('input.value')
217 | mount( )
218 | equal(checkbox.value, 'Bob', 'value property set')
219 | mount( )
220 | equal(checkbox.value, 'Tom', 'value property updated')
221 | mount( )
222 | equal(checkbox.value, '0', 'value property updated')
223 | mount( )
224 | equal(checkbox.value, '', 'value property removed')
225 |
226 | comment('input cursor position')
227 | mount( )
228 | var input = $('input')
229 | input.focus()
230 | input.setSelectionRange(5,7)
231 | mount( )
232 | equal(input.selectionStart, 5, 'selection start')
233 | equal(input.selectionEnd, 7, 'selection end')
234 |
235 | comment('input cursor position on inputs that don\'t support text selection')
236 | mount( )
237 |
238 | comment('input cursor position only the active element')
239 | mount( )
240 | var input = $('input')
241 | input.setSelectionRange(5,7)
242 | if (input.setActive) document.body.setActive()
243 | else input.blur()
244 | mount( )
245 | notEqual(input.selectionStart, 5, 'selection start')
246 | notEqual(input.selectionEnd, 7, 'selection end')
247 |
248 | comment('input.checked')
249 | mount( )
250 | ok(checkbox.checked, 'checked with a true value')
251 | equal(checkbox.getAttribute('checked'), null, 'has checked attribute')
252 | mount( )
253 | ok(!checkbox.checked, 'unchecked with a false value')
254 | ok(!checkbox.hasAttribute('checked'), 'has no checked attribute')
255 | mount( )
256 | ok(checkbox.checked, 'checked with a boolean attribute')
257 | equal(checkbox.getAttribute('checked'), null, 'has checked attribute')
258 | mount( )
259 | ok(!checkbox.checked, 'unchecked when attribute is removed')
260 | ok(!checkbox.hasAttribute('checked'), 'has no checked attribute')
261 |
262 | comment('input.disabled')
263 | mount( )
264 | ok(checkbox.disabled, 'disabled with a true value')
265 | equal(checkbox.hasAttribute('disabled'), true, 'has disabled attribute')
266 | mount( )
267 | equal(checkbox.disabled, false, 'disabled is false with false value')
268 | equal(checkbox.hasAttribute('disabled'), false, 'has no disabled attribute')
269 | mount( )
270 | ok(checkbox.disabled, 'disabled is true with a boolean attribute')
271 | equal(checkbox.hasAttribute('disabled'), true, 'has disabled attribute')
272 | mount( )
273 | equal(checkbox.disabled, false, 'disabled is false when attribute is removed')
274 | equal(checkbox.hasAttribute('disabled'), false, 'has no disabled attribute')
275 |
276 | teardown({renderer,el})
277 | end()
278 | })
279 |
280 | test('option[selected]', ({ok,end,equal}) => {
281 | var {mount,renderer,el} = setup(equal)
282 | var options
283 |
284 | // first should be selected
285 | mount(
286 |
287 | one
288 | two
289 |
290 | )
291 |
292 | options = el.querySelectorAll('option')
293 | ok(!options[1].selected, 'is not selected')
294 | ok(options[0].selected, 'is selected')
295 |
296 | // second should be selected
297 | mount(
298 |
299 | one
300 | two
301 |
302 | )
303 |
304 | options = el.querySelectorAll('option')
305 | ok(!options[0].selected, 'is not selected')
306 | ok(options[1].selected, 'is selected')
307 |
308 | teardown({renderer,el})
309 | end()
310 | })
311 |
312 | test('components', ({equal,end}) => {
313 | var {el,renderer,mount,html} = setup(equal)
314 |
315 | // Object Component
316 | var Test = {
317 | defaultProps: { name: 'Amanda' },
318 | initialState: (props) => ({ text: 'Hello World' }),
319 | render: ({props,state}) => {state.text} ,
320 | afterMount: (c, el, updateState) => updateState({ text: 'Hello Pluto' })
321 | }
322 |
323 | mount( )
324 | var root = el.firstElementChild
325 | equal(root.getAttribute('count'), '2', 'rendered with props')
326 | equal(root.getAttribute('name'), 'Amanda', 'has default props')
327 | equal(root.innerHTML, 'Hello World', 'rendered with initial state')
328 |
329 | mount( )
330 | equal(root.getAttribute('count'), '3', 'props updated')
331 | equal(root.getAttribute('name'), 'Amanda', 'default props still exist')
332 | equal(root.innerHTML, 'Hello Pluto', 'rendered updated state')
333 |
334 | teardown({renderer,el})
335 | equal(el.innerHTML, '', 'the element is removed')
336 | end()
337 | })
338 |
339 | test('simple components', ({equal,end}) => {
340 | var {el,renderer,mount,html} = setup(equal)
341 | var Box = ({props}) => {props.text}
342 | mount( )
343 | html('Hello World
', 'function component rendered')
344 | teardown({renderer,el})
345 | end()
346 | })
347 |
348 | test('nested component lifecycle hooks fire in the correct order', ({deepEqual,mount,end,equal}) => {
349 | var {el,renderer,mount} = setup(equal)
350 | var log = []
351 |
352 | var LifecycleLogger = {
353 | initialState (props) {
354 | log.push(props.name + ' initialState')
355 | return {}
356 | },
357 | beforeMount ({props}) {
358 | log.push(props.name + ' beforeMount')
359 | },
360 | shouldUpdate ({props}) {
361 | log.push(props.name + ' shouldUpdate')
362 | return true
363 | },
364 | beforeUpdate ({props}) {
365 | log.push(props.name + ' beforeUpdate')
366 | },
367 | beforeRender ({props}) {
368 | log.push(props.name + ' beforeRender')
369 | },
370 | validate ({props}) {
371 | log.push(props.name + ' validate')
372 | },
373 | render ({props}) {
374 | log.push(props.name + ' render')
375 | return {props.children}
376 | },
377 | afterRender ({props}) {
378 | log.push(props.name + ' afterRender')
379 | },
380 | afterUpdate ({props}) {
381 | log.push(props.name + ' afterUpdate')
382 | },
383 | afterMount ({props}) {
384 | log.push(props.name + ' afterMount')
385 | },
386 | beforeUnmount ({props}, el) {
387 | log.push(props.name + ' beforeUnmount')
388 | }
389 | }
390 |
391 | mount(
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 | )
400 |
401 | deepEqual(log, [
402 | 'GrandParent initialState',
403 | 'GrandParent validate',
404 | 'GrandParent beforeMount',
405 | 'GrandParent beforeRender',
406 | 'GrandParent render',
407 | 'Parent initialState',
408 | 'Parent validate',
409 | 'Parent beforeMount',
410 | 'Parent beforeRender',
411 | 'Parent render',
412 | 'Child initialState',
413 | 'Child validate',
414 | 'Child beforeMount',
415 | 'Child beforeRender',
416 | 'Child render',
417 | 'Child afterRender',
418 | 'Child afterMount',
419 | 'Parent afterRender',
420 | 'Parent afterMount',
421 | 'GrandParent afterRender',
422 | 'GrandParent afterMount'
423 | ], 'initial render')
424 | log = []
425 |
426 | mount(
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 | )
435 |
436 | deepEqual(log, [
437 | 'GrandParent shouldUpdate',
438 | 'GrandParent beforeUpdate',
439 | 'GrandParent beforeRender',
440 | 'GrandParent validate',
441 | 'GrandParent render',
442 | 'Parent shouldUpdate',
443 | 'Parent beforeUpdate',
444 | 'Parent beforeRender',
445 | 'Parent validate',
446 | 'Parent render',
447 | 'Child shouldUpdate',
448 | 'Child beforeUpdate',
449 | 'Child beforeRender',
450 | 'Child validate',
451 | 'Child render',
452 | 'Child afterRender',
453 | 'Child afterUpdate',
454 | 'Parent afterRender',
455 | 'Parent afterUpdate',
456 | 'GrandParent afterRender',
457 | 'GrandParent afterUpdate'
458 | ], 'updated')
459 | log = []
460 |
461 | mount( )
462 |
463 | deepEqual(log, [
464 | 'GrandParent beforeUnmount',
465 | 'Parent beforeUnmount',
466 | 'Child beforeUnmount'
467 | ], 'unmounted with app.unmount()')
468 |
469 | mount(
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 | )
478 | log = []
479 |
480 | teardown({renderer,el})
481 |
482 | deepEqual(log, [
483 | 'GrandParent beforeUnmount',
484 | 'Parent beforeUnmount',
485 | 'Child beforeUnmount'
486 | ], 'unmounted with renderer.remove()')
487 |
488 | end()
489 | })
490 |
491 | test('component lifecycle hook signatures', ({ok,end,equal}) => {
492 | var {mount,renderer,el} = setup(equal)
493 |
494 | var MyComponent = {
495 | defaultProps: {
496 | count: 0
497 | },
498 | initialState () {
499 | return {
500 | open: true
501 | }
502 | },
503 | beforeMount ({props, state, id}) {
504 | ok(props.count === 0, 'beforeMount has default props')
505 | ok(state.open === true, 'beforeMount has initial state')
506 | ok(id, 'beforeMount has id')
507 | },
508 | shouldUpdate ({props, state, id}, nextProps, nextState) {
509 | ok(props.count === 0, 'shouldUpdate has current props')
510 | ok(state.open === true, 'shouldUpdate has current state')
511 | ok(nextProps.count === 0, 'shouldUpdate has next props')
512 | ok(nextState.open === false, 'shouldUpdate has next state')
513 | return true
514 | },
515 | beforeUpdate ({props, state, id}, nextProps, nextState) {
516 | ok(props.count === 0, 'beforeUpdate has props')
517 | ok(state.open === true, 'beforeUpdate has state')
518 | ok(id, 'beforeUpdate has id')
519 | },
520 | beforeRender ({props, state, id}) {
521 | ok(props, 'beforeRender has props')
522 | ok(state, 'beforeRender has state')
523 | ok(id, 'beforeRender has id')
524 | },
525 | validate ({props, state, id}) {
526 | ok(props, 'validate has props')
527 | ok(state, 'validate has state')
528 | ok(id, 'validate has id')
529 | },
530 | render ({props, state, id}, setState) {
531 | ok(props, 'render has props')
532 | ok(state, 'render has state')
533 | ok(id, 'render has id')
534 | ok(typeof setState === 'function', 'render has state mutator')
535 | return
536 | },
537 | afterRender ({props, state, id}, el) {
538 | ok(props, 'afterRender has props')
539 | ok(state, 'afterRender has state')
540 | ok(id, 'afterRender has id')
541 | ok(el, 'afterRender has DOM element')
542 | },
543 | afterUpdate ({props, state, id}, prevProps, prevState, setState) {
544 | ok(props.count === 0, 'afterUpdate has current props')
545 | ok(state.open === false, 'afterUpdate has current state')
546 | ok(prevProps.count === 0, 'afterUpdate has previous props')
547 | ok(prevState.open === true, 'afterUpdate has previous state')
548 | ok(typeof setState === 'function', 'afterUpdate can update state')
549 | ok(id, 'afterUpdate has id')
550 | },
551 | afterMount ({props, state, id}, el, setState) {
552 | ok(props, 'afterMount has props')
553 | ok(state, 'afterMount has state')
554 | ok(id, 'afterMount has id')
555 | ok(el, 'afterMount has DOM element')
556 | ok(typeof setState === 'function', 'afterMount can update state')
557 | ok(document.getElementById('foo'), 'element is in the DOM')
558 | setState({ open: false })
559 | },
560 | beforeUnmount ({props, state, id}, el) {
561 | ok(props, 'beforeUnmount has props')
562 | ok(state, 'beforeUnmount has state')
563 | ok(id, 'beforeUnmount has id')
564 | ok(el, 'beforeUnmount has el')
565 | end()
566 | }
567 | }
568 |
569 | mount( )
570 | teardown({renderer,el})
571 | })
572 |
573 | test('replace props instead of merging', ({equal,end}) => {
574 | var {mount,renderer,el} = setup(equal)
575 | mount( )
576 | mount( )
577 | equal(el.innerHTML, ' Pluto ')
578 | teardown({renderer,el})
579 | end()
580 | })
581 |
582 | test(`should update all children when a parent component changes`, ({equal,end}) => {
583 | var {mount,renderer,el} = setup(equal)
584 | var parentCalls = 0
585 | var childCalls = 0
586 |
587 | var Child = {
588 | render: function({props, state}){
589 | childCalls++
590 | return {props.text}
591 | }
592 | }
593 |
594 | var Parent = {
595 | render: function({props, state}){
596 | parentCalls++
597 | return (
598 |
599 |
600 |
601 | )
602 | }
603 | }
604 |
605 | mount( )
606 | mount( )
607 | equal(childCalls, 2, 'child rendered twice')
608 | equal(parentCalls, 2, 'parent rendered twice')
609 | teardown({renderer,el})
610 | end()
611 | })
612 |
613 | test('update nested components when state changes', assert => {
614 | var app = deku();
615 | app.mount( )
616 | var container = div()
617 | var rendered = render(app, container)
618 | assert.equal(container.innerHTML, 'foo
', 'initial render')
619 | raf(function(){
620 | assert.equal(container.innerHTML, 'bar
', 'updated on the next frame')
621 | rendered.remove()
622 | assert.end()
623 | })
624 | })
625 |
626 | test('batched rendering', assert => {
627 | var i = 0
628 | var IncrementAfterUpdate = {
629 | render: function(){
630 | return
631 | },
632 | afterUpdate: function(){
633 | i++
634 | }
635 | }
636 | var el = document.createElement('div')
637 | var app = deku()
638 | app.mount( )
639 | var renderer = render(app, el)
640 | app.mount( )
641 | app.mount( )
642 | raf(function(){
643 | assert.equal(i, 1, 'rendered *once* on the next frame')
644 | renderer.remove()
645 | assert.end()
646 | })
647 | })
648 |
649 | test('rendering nested components', ({equal,end}) => {
650 | var {mount,renderer,el,html} = setup(equal)
651 |
652 | var ComponentA = ({props}) => {props.children}
653 | var ComponentB = ({props}) => {props.children}
654 |
655 | var ComponentC = ({props}) => {
656 | return (
657 |
658 |
659 |
660 | {props.text}
661 |
662 |
663 |
664 | )
665 | }
666 |
667 | mount( )
668 | html('', 'element is rendered')
669 | mount( )
670 | equal(el.innerHTML, '', 'element is updated with props')
671 | teardown({renderer,el})
672 | html('', 'element is removed')
673 | end()
674 | })
675 |
676 | test('rendering new elements should be batched with state changes', ({equal,end}) => {
677 | var app = deku()
678 | var el = div()
679 | var renderer = render(app, el)
680 | var mount = app.mount.bind(app)
681 | var unmount = app.unmount.bind(app)
682 | var emitter = new Emitter()
683 | var i = 0
684 |
685 | var ComponentA = {
686 | initialState: function(){
687 | return {
688 | text: 'Deku Shield'
689 | }
690 | },
691 | afterMount: function(component, el, updateState) {
692 | emitter.on('data', text => updateState({ text: text }))
693 | },
694 | render: function({props,state}){
695 | i++
696 | return {props.text} {state.text}
697 | }
698 | }
699 |
700 | var ComponentB = {
701 | render: function({props,state}){
702 | i++
703 | return
704 | }
705 | }
706 |
707 | mount( )
708 |
709 | raf(function(){
710 | emitter.emit('data', 'Mirror Shield')
711 | mount( )
712 | raf(function(){
713 | equal(i, 4, 'rendering was batched')
714 | equal(el.innerHTML, ``, 'rendered correctly')
715 | teardown({renderer,el})
716 | end()
717 | })
718 | })
719 | })
720 |
721 | test('skipping updates with shouldUpdate', ({equal,end,fail}) => {
722 | var {mount,renderer,el} = setup(equal)
723 |
724 | var Test = {
725 | afterUpdate: () => fail('component was updated'),
726 | shouldUpdate: () => false,
727 | render: () =>
728 | }
729 |
730 | mount( )
731 | mount( )
732 | teardown({renderer,el})
733 | end()
734 | })
735 |
736 | test('skipping updates when the same virtual element is returned', ({equal,end,fail,pass}) => {
737 | var {mount,renderer,el} = setup(equal)
738 | var el =
739 |
740 | var Component = {
741 | render (component) {
742 | return el
743 | },
744 | afterUpdate () {
745 | fail('component was updated')
746 | }
747 | }
748 |
749 | mount( )
750 | mount( )
751 | pass('component not updated')
752 | teardown({renderer,el})
753 | end()
754 | })
755 |
756 | test('should empty the container before initial render', assert => {
757 | var el = div()
758 | el.innerHTML = 'a
'
759 | var app = deku(b
)
760 | var renderer = render(app, el)
761 | assert.equal(el.innerHTML, 'b
', 'container was emptied')
762 | renderer.remove()
763 | assert.end()
764 | })
765 |
766 | test('unmount sub-components that move themselves in the DOM', ({equal,end}) => {
767 | var {mount,renderer,el} = setup(equal)
768 | var arr = []
769 |
770 | var Overlay = {
771 | afterMount: (component, el) => {
772 | document.body.appendChild(el)
773 | },
774 | beforeUnmount: function(){
775 | arr.push('A')
776 | },
777 | render: function(){
778 | return
779 | }
780 | }
781 |
782 | var Parent = {
783 | render: ({props, state}) => {
784 | if (props.show) {
785 | return (
786 |
787 |
788 |
789 | )
790 | } else {
791 | return
792 | }
793 | }
794 | }
795 |
796 | mount( )
797 | var overlay = document.querySelector('.Overlay')
798 | equal(overlay.parentElement, document.body, 'element was moved in the DOM')
799 | mount( )
800 | equal(arr[0], 'A', 'unmount was called')
801 | teardown({renderer,el})
802 | end()
803 | })
804 |
805 | test('firing mount events on sub-components created later', ({equal,pass,end,plan}) => {
806 | var {mount,renderer,el} = setup(equal)
807 |
808 | var ComponentA = {
809 | render: () =>
,
810 | beforeUnmount: () => pass('beforeUnmount called'),
811 | beforeMount: () => pass('beforeMount called'),
812 | afterMount: () => pass('afterMount called')
813 | }
814 |
815 | plan(3)
816 | mount( )
817 | mount(
)
818 | teardown({renderer,el})
819 | end()
820 | })
821 |
822 | test('should change root node and still update correctly', ({equal,end}) => {
823 | var {mount,html,renderer,el} = setup(equal)
824 |
825 | var ComponentA = ({props}) => dom(props.type, null, props.text)
826 | var Test = ({props}) =>
827 |
828 | mount( )
829 | html('test ')
830 | mount( )
831 | html('test
')
832 | mount( )
833 | html('foo
')
834 | teardown({renderer,el})
835 | end()
836 | })
837 |
838 | test('replacing components with other components', ({equal,end}) => {
839 | var {mount,renderer,el,html} = setup(equal)
840 | var ComponentA = () => A
841 | var ComponentB = () => B
842 |
843 | var ComponentC = ({props,state}) => {
844 | if (props.type === 'A') {
845 | return
846 | } else {
847 | return
848 | }
849 | }
850 |
851 | mount( )
852 | html('A
')
853 | mount( )
854 | html('B
')
855 | teardown({renderer,el})
856 | end()
857 | })
858 |
859 | test('adding, removing and updating events', ({equal,end}) => {
860 | var {mount,renderer,el,$} = setup(equal)
861 | var count = 0
862 | var onclicka = () => count += 1
863 | var onclickb = () => count -= 1
864 |
865 | var Page = {
866 | render: ({props}) =>
867 | }
868 |
869 | mount( )
870 | trigger($('span'), 'click')
871 | equal(count, 1, 'event added')
872 | mount( )
873 | trigger($('span'), 'click')
874 | equal(count, 0, 'event updated')
875 | mount( )
876 | trigger($('span'), 'click')
877 | equal(count, 0, 'event removed')
878 | teardown({renderer,el})
879 | end()
880 | })
881 |
882 | test('should bubble events', ({equal,end,fail,ok}) => {
883 | var {mount,renderer,el,$} = setup(equal)
884 |
885 | var Test = {
886 | render: function ({props,state}) {
887 | return (
888 |
893 | )
894 | }
895 | }
896 |
897 | var onClickTest = function (event, component, setState) {
898 | setState({ active: true })
899 | equal(el.firstChild.firstChild, event.delegateTarget, 'event.delegateTarget is set')
900 | return false
901 | }
902 |
903 | var onParentClick = function () {
904 | fail('event bubbling was not stopped')
905 | }
906 |
907 | mount( )
908 | trigger($('a'), 'click')
909 | ok($('.active'), 'event fired on parent element')
910 | teardown({renderer,el})
911 | end()
912 | })
913 |
914 | test('unmounting components when removing an element', ({equal,pass,end,plan}) => {
915 | var {mount,renderer,el} = setup(equal)
916 |
917 | var Test = {
918 | render: () =>
,
919 | beforeUnmount: () => pass('component was unmounted')
920 | }
921 |
922 | plan(1)
923 | mount()
924 | mount(
)
925 | teardown({renderer,el})
926 | end()
927 | })
928 |
929 | test('update sub-components with the same element', ({equal,end}) => {
930 | var {mount,renderer,el} = setup(equal)
931 |
932 | let Page1 = {
933 | render({ props }) {
934 | return (
935 |
936 |
937 |
938 | {
939 | props.show ?
940 |
941 |
942 |
943 |
944 | :
945 |
946 | Hello
947 |
948 | }
949 |
950 |
951 |
952 | )
953 | }
954 | }
955 |
956 | let Page2 = ({props}) => {
957 | return (
958 |
959 | {props.title}
960 |
961 | )
962 | }
963 |
964 | let App = ({props}) => props.page === 1 ? :
965 |
966 | mount( )
967 | mount( )
968 | mount( )
969 | mount( )
970 | equal(el.innerHTML, 'foo
')
971 | teardown({renderer,el})
972 | end()
973 | })
974 |
975 | test('replace elements with component nodes', ({equal,end}) => {
976 | var {mount,renderer,el} = setup(equal)
977 | mount( )
978 | equal(el.innerHTML, ' ', 'rendered element')
979 | mount(component )
980 | equal(el.innerHTML, 'component
', 'replaced with component')
981 | teardown({renderer,el})
982 | end()
983 | })
984 |
985 | test('svg elements', ({equal,end}) => {
986 | var {mount,renderer,el} = setup(equal)
987 | mount(
988 |
989 |
990 |
991 |
992 |
993 | )
994 | equal(el.firstChild.tagName, 'svg', 'rendered svg element')
995 | teardown({renderer,el})
996 | end()
997 | })
998 |
999 | test('moving components with keys', ({equal,end,ok,pass,plan}) => {
1000 | var {mount,renderer,el} = setup(equal)
1001 | var one,two,three
1002 |
1003 | plan(10)
1004 |
1005 | mount(
1006 |
1007 | One
1008 | Two
1009 |
1010 | )
1011 | var [one,two] = el.querySelectorAll('li')
1012 |
1013 | // Moving
1014 | mount(
1015 |
1016 | Two
1017 | One
1018 |
1019 | )
1020 | var updated = el.querySelectorAll('li')
1021 | ok(updated[1] === one, 'foo moved down')
1022 | ok(updated[0] === two, 'bar moved up')
1023 |
1024 | // Removing
1025 | mount(
1026 |
1029 | )
1030 | updated = el.querySelectorAll('li')
1031 | ok(updated[0] === two && updated.length === 1, 'foo was removed')
1032 |
1033 | // Updating
1034 | mount(
1035 |
1036 | One
1037 | Two
1038 | Three
1039 |
1040 | )
1041 | var [one,two,three] = el.querySelectorAll('li')
1042 | mount(
1043 |
1044 | One
1045 | Four
1046 |
1047 | )
1048 | var updated = el.querySelectorAll('li')
1049 | ok(updated[0] === one, 'foo is the same')
1050 | ok(updated[1] === three, 'baz is the same')
1051 | ok(updated[1].innerHTML === 'Four', 'baz was updated')
1052 | var foo = updated[0]
1053 | var baz = updated[1]
1054 |
1055 | // Adding
1056 | mount(
1057 |
1058 | One
1059 | Five
1060 | Four
1061 |
1062 | )
1063 | var updated = el.querySelectorAll('li')
1064 | ok(updated[0] === foo, 'foo is the same')
1065 | ok(updated[2] === baz, 'baz is the same')
1066 | ok(updated[1].innerHTML === 'Five', 'bar was added')
1067 |
1068 | // Moving event handlers
1069 | var clicked = () => pass('event handler moved')
1070 | mount(
1071 |
1072 | One
1073 |
1074 | Click Me!
1075 |
1076 |
1077 | )
1078 | mount(
1079 |
1080 |
1081 | Click Me!
1082 |
1083 | One
1084 |
1085 | )
1086 | trigger(el.querySelector('span'), 'click')
1087 |
1088 | // Removing handlers. If the handler isn't removed from
1089 | // the path correctly, it will still fire the handler from
1090 | // the previous assertion.
1091 | mount(
1092 |
1093 |
1094 | One
1095 |
1096 |
1097 | )
1098 | trigger(el.querySelector('span'), 'click')
1099 |
1100 | teardown({renderer,el})
1101 | end()
1102 | })
1103 |
1104 | test('updating event handlers when children are removed', ({equal,end}) => {
1105 | var {mount,renderer,el} = setup(equal)
1106 | var items = ['foo','bar','baz']
1107 |
1108 | var ListItem = ({props}) => {
1109 | return (
1110 |
1111 | items.splice(props.index, 1)} />
1112 |
1113 | )
1114 | }
1115 |
1116 | var List = ({props}) => {
1117 | return (
1118 |
1119 | {props.items.map((_,i) => )}
1120 |
1121 | )
1122 | }
1123 |
1124 | mount(
)
1125 | trigger(el.querySelector('a'), 'click')
1126 | mount(
)
1127 | trigger(el.querySelector('a'), 'click')
1128 | mount(
)
1129 | trigger(el.querySelector('a'), 'click')
1130 | mount(
)
1131 | equal(el.innerHTML, '', 'all items were removed')
1132 |
1133 | teardown({renderer,el})
1134 | end()
1135 | })
1136 |
1137 | // Let's run this test only if the browser supports shadow DOM
1138 | if (document.body.createShadowRoot) {
1139 | test('change the root listener node so we can render into document fragments', ({equal,end}) => {
1140 | var Button = {
1141 | render: function(comp) {
1142 | return dom('button', { onClick: () => end() })
1143 | }
1144 | }
1145 |
1146 | var host = document.createElement('div')
1147 | var shadow = host.createShadowRoot()
1148 | shadow.innerHTML = '
'
1149 | var mountNode = shadow.querySelector('div')
1150 |
1151 | document.body.appendChild(host)
1152 |
1153 | var app = deku( )
1154 | var renderer = render(app, mountNode, { batching: false })
1155 | var button = shadow.querySelector('button')
1156 | trigger(button, 'click')
1157 |
1158 | renderer.remove()
1159 | document.body.removeChild(host)
1160 | })
1161 | }
1162 |
1163 | /**
1164 | * Sources.
1165 | * This feature will be removed in a future version. It's kept in now
1166 | * for backwards-compatibility
1167 | */
1168 |
1169 | test('should set source without property type', ({equal,end}) => {
1170 | var {mount,renderer,el,app} = setup(equal)
1171 |
1172 | const App = {
1173 | propTypes: {
1174 | foo: {
1175 | source: 'foo'
1176 | }
1177 | },
1178 | render({props}) {
1179 | return (
1180 |
1181 | {props.foo || 'App'}
1182 |
1183 | );
1184 | }
1185 | }
1186 |
1187 | app.set('foo', 'bar')
1188 | mount( )
1189 | equal(el.innerHTML, 'bar
')
1190 | teardown({renderer,el})
1191 | end()
1192 | })
1193 |
1194 | test('should handle removing entities', ({equal,end}) => {
1195 | var {mount,renderer,el,app} = setup(equal)
1196 |
1197 | const App = {
1198 | propTypes: {
1199 | foo: { source: 'foo' }
1200 | },
1201 | render({props}) {
1202 | let {foo} = props
1203 | let page = foo ? :
1204 | return {page}
1205 | }
1206 | }
1207 |
1208 | const Page1 = {
1209 | propTypes: {
1210 | foo: { source: 'foo' }
1211 | },
1212 | render() {
1213 | return Page1
1214 | }
1215 | }
1216 |
1217 | const Page2 = {
1218 | propTypes: {
1219 | foo: { source: 'foo' }
1220 | },
1221 | render(component) {
1222 | return Page2
1223 | }
1224 | }
1225 |
1226 | app.set('foo', 'bar');
1227 | mount( )
1228 | equal(el.innerHTML, '')
1229 | app.set('foo', false);
1230 | equal(el.innerHTML, '')
1231 | teardown({renderer,el})
1232 | end()
1233 | })
1234 |
1235 | test('should get default value from data value', ({equal,end}) => {
1236 | var {mount,renderer,el,app} = setup(equal)
1237 |
1238 | var Test = {
1239 | propTypes: {
1240 | 'data': { source: 'meta' }
1241 | },
1242 | render: function({props,state}) {
1243 | return {props.data.title}
1244 | }
1245 | }
1246 |
1247 | app.set('meta', { title: 'Hello World' })
1248 | mount( )
1249 | equal(el.innerHTML, 'Hello World
')
1250 | teardown({renderer,el})
1251 | end()
1252 | });
1253 |
1254 | test('should update with new value from data source', ({equal,end}) => {
1255 | var {mount,renderer,el,app} = setup(equal)
1256 |
1257 | var Test = {
1258 | propTypes: {
1259 | text: { source: 'title' }
1260 | },
1261 | render: function({props,state}) {
1262 | return {props.text}
1263 | }
1264 | }
1265 |
1266 | app.set('title', 'Hello World')
1267 | mount( )
1268 | equal(el.innerHTML, 'Hello World
')
1269 | app.set('title', 'Hello Pluto')
1270 | equal(el.innerHTML, 'Hello Pluto
')
1271 | teardown({renderer,el})
1272 | end()
1273 | })
1274 |
1275 | test('should handle two-way updating', ({equal,end}) => {
1276 | var {mount,renderer,el,app} = setup(equal)
1277 |
1278 | var Test = {
1279 | propTypes: {
1280 | 'text': { source: 'title' },
1281 | 'updateTitle': { source: 'setTitle' }
1282 | },
1283 | render: function({props,state}) {
1284 | return dom('div', { onClick: onClick }, props.text);
1285 | function onClick() {
1286 | props.updateTitle('Hello Pluto');
1287 | }
1288 | }
1289 | }
1290 |
1291 | function setTitle(string) {
1292 | app.set('title', string);
1293 | }
1294 |
1295 | app.set('title', 'Hello World')
1296 | app.set('setTitle', setTitle)
1297 | app.mount( )
1298 | equal(el.innerHTML, 'Hello World
')
1299 | trigger(el.querySelector('div'), 'click')
1300 | equal(el.innerHTML, 'Hello Pluto
')
1301 | teardown({renderer,el})
1302 | end()
1303 | })
1304 |
1305 | test('should handle two-way updating with multiple components depending on the same source', ({equal,end}) => {
1306 | var {mount,renderer,el,app} = setup(equal)
1307 |
1308 | var TestA = {
1309 | propTypes: {
1310 | 'text': { source: 'title' },
1311 | 'updateTitle': { source: 'setTitle' }
1312 | },
1313 | render: function({props,state}) {
1314 | return dom('span', { onClick: onClick }, props.text);
1315 | function onClick() {
1316 | props.updateTitle('Hello Pluto');
1317 | }
1318 | }
1319 | }
1320 |
1321 | var TestB = {
1322 | propTypes: {
1323 | 'text': { source: 'title' },
1324 | },
1325 | render: function({props,state}) {
1326 | return dom('span', null, props.text);
1327 | }
1328 | }
1329 |
1330 | function setTitle(string) {
1331 | app.set('title', string);
1332 | }
1333 |
1334 | app.set('title', 'Hello World')
1335 | app.set('setTitle', setTitle)
1336 | app.mount(
)
1337 | equal(el.innerHTML, 'Hello World Hello World
')
1338 | trigger(el.querySelector('span'), 'click')
1339 | equal(el.innerHTML, 'Hello Pluto Hello Pluto
')
1340 | teardown({renderer,el})
1341 | end()
1342 | })
1343 |
--------------------------------------------------------------------------------