diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..9e27feb --- /dev/null +++ b/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": ["es2015", "stage-2"], + "sourceMaps": true, + "plugins": [ + ["transform-react-jsx", {"pragma": "h"}] + ] +} diff --git a/.gitignore b/.gitignore index f6467ff..daf7a12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules -components .DS_Store +_book diff --git a/.npmignore b/.npmignore index d55582f..121ef01 100644 --- a/.npmignore +++ b/.npmignore @@ -1,8 +1,2 @@ -node_modules -test/ -docs/ -.npmignore -.zuul -History.md -LICENSE.md -Makefile \ No newline at end of file +test +src diff --git a/.zuul.yml b/.zuul.yml index 002c144..3d476b5 100644 --- a/.zuul.yml +++ b/.zuul.yml @@ -2,9 +2,9 @@ ui: tape concurrency: 2 browsers: - name: chrome - version: 43..latest + version: latest - name: firefox - version: 38..latest + version: latest - name: iphone version: latest - name: ipad @@ -15,4 +15,3 @@ browsers: version: 7..latest browserify: - transform: babelify - - transform: envify diff --git a/History.md b/History.md deleted file mode 100644 index 3a504e2..0000000 --- a/History.md +++ /dev/null @@ -1,478 +0,0 @@ - -1.0.0 / 2015-12-04 -================== - - * Add load event - * fix counter example - * Fixed bug with adding events - * Add HTML5 media events - * add babel 6 instructions yet point back to babel@5 - * alias npm test script to make test for sake of convention - * add to install guide using webpack - * Don't treat number as a falsy attribute - * Add failing test for input values - * getting examples working using duo-serve - -0.5.6 / 2015-09-21 -================== - - * meta: updating dist for duo/component - -0.5.5 / 2015-09-11 -================== - - * return result of calling event handler (return `false` to cancel bubbling) - -0.5.4 / 2015-08-14 -================== - - * Merge pull request #232 from dekujs/stringify/empty-attrs - * properly handling empty attrs, with tests to prevent regression - * Merge pull request #231 from dekujs/stringify/no-children - * fixing stringify for components with children - * Update events.js - -0.5.3 / 2015-08-12 -================== - - * Merge pull request #230 from dekujs/text-selection-active-only - * ie handles focus differently, so gotta make sure we account for that - * only adjust text selection for the active element - -0.5.2 / 2015-08-12 -================== - - * Fixed issue selecting text with some input types - * Update history. I'll fix that makefile one day - -0.5.1 / 2015-08-11 -================== - - * adding some helpful form-related events - * Corrected afterMount arity. Fixes #225. - * Keep cursor position when changing input value - -0.5.0 / 2015-08-03 -================== - - * 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. - * Components can be functions instead of objects - * Update dependencies - * Fixed bug with replacing text nodes with undefined - * Refactored the tests - * Removed DOM pooling - * Switch to use virtual-element - -0.4.12 / 2015-07-28 -================== - - * Merge pull request #212 from dekujs/flatmap-children - * Fixed failing tests - * Update index.js - * Flattening the virtual element children array - -0.4.11 / 2015-07-17 -================== - - * Fixed deprecation warnings - * Updated history - -0.4.10 / 2015-07-16 -================== - - * Added validate hook - * Attach events to document instead of document.body - * added ability to cancel events - * Possibility to pass `false` as well as `null` in component children - * Remove prop validation - * Added deprecation warnings for magic class and style transformations. - * No longer flattening children in virtual nodes - * Faster SVG element lookups - -0.4.9 / 2015-07-07 -================== - - * Merge pull request #191 from timmak/svg-missing-animate - * Add animate to svg list - -0.4.8 / 2015-07-01 -================== - - * Merge branch 'master' of ssh://github.com/dekujs/deku - * Merge pull request #188 from xdissent/fix-remove-null-el - * Handle null element in isElement(). fixes #180 - -0.4.7 / 2015-07-01 -================== - - * Not pooling select elements - -0.4.6 / 2015-06-29 -================== - - * Merge pull request #187 from dekujs/should-render - * Fixed: State not committed during shouldUpdate - * Merge pull request #177 from mpal9000/patch-1 - * docs - initialState props - -0.4.5 / 2015-06-13 -================== - - * We made it smaller! - * Merge pull request #173 from foray1010/master - * Added wheel event - * Update README.md - * Update README.md - * Merge pull request #166 from xdissent/patch-1 - * Merge pull request #167 from DylanPiercey/patch-1 - * Update jsx.md - * Update events link in README - -0.4.4 / 2015-06-05 -================== - - * Added `createElement` alias for `element` - * Update components.md - * Updated changelog - -0.4.3 / 2015-06-04 -================== - - * Remove event throttling. Fixes #159 - * added keypress event - * Fixed issue with rendering event handlers when using renderString - -0.4.2 / 2015-05-28 -================== - - * fixed event handling so events bubble to parent handlers - -0.4.1 / 2015-05-26 -================== - - * propTypes validation - support for array of types, type as function - -0.4.0 / 2015-05-22 -================== - - * Fixed: Fixed issue with components rendered as root nodes. - * New: initialState now takes the props as a param - * 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. - * New: You can nest propTypes now. Just set the `type` field to be another propTypes object. - * Fixed: `afterRender` and `afterMount` are now called when the element is in the DOM. - * Updated: Added phantomjs to the dev deps - -0.3.3 / 2015-05-22 -================== - - * Added mouseenter/mouseleave - * Merge pull request #137 from Frikki/issue-134/modular-fastjs - * Replaced fast.js require with modular requirement. - -0.3.2 / 2015-05-20 -================== - - - -0.3.1 / 2015-05-21 -================== - - * fixed error with swapping component using sources - -0.3.0 / 2015-05-18 -================== - - * Added: warnings and nicer error messages - * Added: Always emptying the container when rendering - * Added: Deku.d.ts file - * Removed: the `defaultX` attributes from checkboxes, selects and inputs - * Fixed: rendering for `checked`, `selected` and `disabled` attributes - * Fixed: multiple components depending on the same `source` - -0.2.17 / 2015-05-11 -================== - - * set sources on update - -0.2.16 / 2015-05-11 -================== - - * Using a different component object each render - * Cleaned up tests and build - * Calling .set will always trigger an update instead of checking equality with the previous data value. - * Added React comparison examples - * Fixed bug where handler references weren't removed - * Skip rendering if element is the same - -0.2.15 / 2015-05-04 -================== - - * add svg support - * Throw errors for empty types on elements - -0.2.14 / 2015-05-03 -================== - - * Using fast.js - * Added some simple examples - -0.2.13 / 2015-04-30 -================== - - * Added workaround for diffChildren bug - -0.2.12 / 2015-04-29 -================== - - * Merge pull request #83 from segmentio/fix/child-key-diffing - * Removed key diffing for elements - * Passing tests for the keys with events - * Tests passing with janky first version - * Added failing test - * Only flatten children one level deep - -0.2.11 / 2015-04-29 -================== - - * Added test for virtual node indexes - * Correctly casting key to a string - -0.2.10 / 2015-04-29 -================== - - * Improved performance by removing 'omit' - * IE10 fix - * Running all tests - * Added tests for components with keys - * Coercing keys to strings - * Code style - -0.2.9 / 2015-04-28 -================== - - * Passing tests - * The patch returns the updated element - * Code style - * Removing elements first when diffing - * Cleaned up the key diffing - -0.2.8 / 2015-04-28 -================== - - * Fixed more issues with falsy keys - -0.2.7 / 2015-04-28 -================== - - * Fixed falsy keys - -0.2.6 / 2015-04-28 -================== - - * Fixed incorrect path - -0.2.5 / 2015-04-28 -================== - - * Avoid touching elements that haven't moved - * Added test for adding nodes with new keys - -0.2.4 / 2015-04-28 -================== - - * Fixed bug with creating new nodes in the diff - -0.2.3 / 2015-04-27 -================== - - * Fixed issue with initial mount - -0.2.2 / 2015-04-27 -================== - - * Merge branch 'docs' - * Updated docs - * Removed old docs and examples - * Allowing easier initial mounting - * Adding docs - -0.2.1 / 2015-04-25 -================== - - * Fixed bug with diffing keyed nodes - -0.2.0 / 2015-04-25 -================== - - * Updated the hook API - * Removed defaults - -0.1.1 / 2015-04-22 -================== - - * Replaced lodash and removed unused modules - -0.1.0 / 2015-04-21 -================== - -Breaking - * Updated the top-level API. It now mounts virtual nodes instead of components directly. - * Removed the `component()` DSL. Components are just objects now with no concept of `this`. This is one step towards making hook functions pure. - * 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). - * Removed tagName parsing (eg. `dom('div.foo')`) as it was slowing things down - -New Features - * Added key diffing using the `key` attribute on virtual nodes - * Added optional prop validation via `propTypes` - * Added defaultProps hook to components - * Added the ability for components to access data on the app. This makes it easy to side-load data. - -Fixes - * Fixed bug with inputs/textarea being pooled incorrectly - * Merge pull request #72 from segmentio/attr-modification-bug - * Fixed a bug in the renderer for falsy attributes - * Numerous speed improvements - * Fixed bug with string renderer not calling `beforeMount` - * Removed the raf loop and just batches - -0.0.33 / 2015-04-02 -================== - - * Fixed bug with nested components not being unmounted - * Added test for nested components disabling pooling - -0.0.32 / 2015-04-01 -================== - - * dom: Fixed disablePooling flag for nested components - -0.0.31 / 2015-04-01 -================== - - * dom: Tests for scene removal - * dom: Cleaned up removing of elements - -0.0.30 / 2015-03-27 -================== - - * Added DOM pooling - -0.0.29 / 2015-03-24 -================== - - * Breaking change: Updated the scene/renderer API to allow for more powerful plugins. The Component API is now decoupled from the renderer. - * Tests now using ES6 - * Fixed beforeMount not firing with renderString - * Fixed innerHTML rendering with renderString - -0.0.28 / 2015-03-11 -================== - - * Interactions bind to the body - * Update component#render() to throw if container is empty - * Fixed tests in IE9. Fixed SauceLab tests. - * Removed all the crap from the repo - -0.0.27 / 2015-02-26 -================== - - * Fixed bug with re-rendering child nodes - -0.0.26 / 2015-02-26 -================== - -* 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. -* The scene continues to update on every frame, but will still only actually render a component in the tree has changed. -* 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. -* 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. -* The entities now don't know about the scene at all, making them completely decoupled from it. -* 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. -* The scene will now pause when there is an error during rendering to prevent endless errors. -* 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. -* The diff is now slightly more decoupled, which will allow it to be extracted from deku. -* Removed some unused dependencies. This should make the whole library smaller. -* The logic around commiting changes to props/state in the entity has been reworked. It's now much simpler and less prone to bugs. - - -0.0.25 / 2015-02-22 -================== - - * JSX support - -0.0.24 / 2015-02-11 -================== - - * Added failing test for nested events - * added hasFunction to fix #47 - * added failing test to demo function diffing - -0.0.23 / 2015-02-09 -================== - - * Added innerHTML support - * Fixed drag and drop test event - * Added test for #47 - -0.0.22 / 2015-02-03 -================== - - * Pulled virtual DOM lib out - -0.0.21 / 2015-02-03 -================== - - * Added ability to render component at the root - -0.0.20 / 2015-01-29 -================== - - * Value attribute gets a special case in the diff - * Using raf-loop instead of local module - * Using uid module - -0.0.19 / 2015-01-25 -================== - - * Moved to browserify for the build - -0.0.18 / 2015-01-25 -================== - - * Fixed event delegation - * Added some super basic perf tests - * Fixed issue with scene not removing listeners - -0.0.17 / 2015-01-24 -================== - - * Fixed bug when changing root node. Closes #33 - -0.0.16 / 2015-01-23 -================== - - * Fixed issue with channels not being sent to render - -0.0.15 / 2015-01-23 -================== - - * Added .channel and .prop methods - * Removed .send and .onMessage in favour of channels - * Scene is no longer immediately rendered - -0.0.14 / 2015-01-21 -================== - - * 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); - -0.0.13 / 2015-01-20 -================== - - * Fixed a bug with IDs being identical - * Added History.md - -0.0.12 / 2015-01-19 -================== - - * Add repo field - * Updated bump - * Updated release task diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index e5f2164..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -The MIT License (MIT) Copyright (c) 2015 Anthony Short - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index db97944..a74cb84 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,11 @@ # -# Binaries. +# Vars. # -export PATH := ./node_modules/.bin:${PATH} BIN := ./node_modules/.bin -# -# Wildcards. -# - -src = $(shell find lib/*.js) -tests = $(shell find test/**/*.js) +src = $(shell find src/*.js) +tests = $(shell find test/*.js) # # Targets. @@ -20,54 +15,34 @@ default: test $(src): node_modules $(tests): node_modules -standalone: $(src) - @mkdir -p build - @NODE_ENV=production browserify \ +build: $(src) + @mkdir -p dist + @NODE_ENV=production ${BIN}/browserify \ --standalone deku \ - -t envify \ - -e lib/index.js | bfc > build/deku.js + -t babelify \ + -e src/index.js > dist/deku.js -test: $(src) $(tests) - @NODE_ENV=development hihat test/index.js -- \ +test: lint + @NODE_ENV=development ${BIN}/hihat test/index.js -- \ --debug \ - -t envify \ -t babelify \ -p tap-dev-tool -test-cloud: node_modules - @TRAVIS_BUILD_NUMBER=$(CIRCLE_BUILD_NUM) zuul -- ./test/index.js +ci: node_modules lint + @TRAVIS_BUILD_NUMBER=$(CIRCLE_BUILD_NUM) ${BIN}/zuul -- ./test/index.js node_modules: package.json @npm install -clean: - @-rm -rf build build.js node_modules - lint: $(src) $(tests) - standard lib/**/*.js | snazzy - -size: standalone - @minify build/deku.js | gzip -9 | wc -c - -# -# Releases. -# + ${BIN}/standard src/*.js test/*.js | ${BIN}/snazzy -release: standalone - bump $$VERSION && \ - git changelog --tag $$VERSION && \ - git commit --all -m "Release $$VERSION" && \ - git tag $$VERSION && \ - git push origin master --tags && \ - npm publish +docs: + gitbook serve # -# These tasks will be run every time regardless of dependencies. +# Always run these tasks. # -.PHONY: standalone -.PHONY: clean -.PHONY: lint -.PHONY: size -.PHONY: release -.PHONY: test-cloud +.PHONY: build +.PHONY: docs diff --git a/README.md b/README.md index 01aa084..c9e1d61 100644 --- a/README.md +++ b/README.md @@ -1,207 +1,63 @@ -# Deku +# Deku [![Circle CI](https://circleci.com/gh/dekujs/deku/tree/2.0.0.svg?style=svg)](https://circleci.com/gh/dekujs/deku/tree/2.0.0) -[![version](https://img.shields.io/npm/v/deku.svg?style=flat-square)](https://www.npmjs.com/package/deku) [![Circle CI](https://img.shields.io/circleci/project/BrightFlair/PHP.Gt.svg?style=flat-square)](https://circleci.com/gh/dekujs/deku) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) [![Chat](https://img.shields.io/badge/Discord-Join%20Chat%20→-blue.svg?style=flat-square)](https://discord.gg/0gNkyCAVkDYsBaFe) +[![version](https://img.shields.io/npm/v/deku.svg?style=flat-square)](https://www.npmjs.com/package/deku) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) +[![npm downloads](https://img.shields.io/npm/dm/deku.svg?style=flat-square)](https://www.npmjs.com/package/deku) -A library for creating UI components using virtual DOM as an alternative to [React](https://github.com/facebook/react). Deku has a smaller footprint (~6kb), a functional API, and doesn't support legacy browsers. +Deku is a library for rendering interfaces using pure functions. -``` -npm install deku virtual-element -``` - -You can also use Duo, Bower or [download the files manually](https://github.com/dekujs/deku/releases). - -## Example - -```js -import element from 'virtual-element' -import {render,tree} from 'deku' -import MyButton from './button' - -var app = tree( -
- Hello World! -
-) - -render(app, document.body) -``` +Instead of using classes and local state, Deku just uses functions and pushes the responsibility of all state management and side-effects onto tools like [Redux](http://redux.js.org/). It also aims to support only modern browsers to keep things simple. -## Introduction +It can be used in place of libraries like React and works well with Redux and other libraries in the React ecosystem. -Deku is a DOM renderer for virtual elements that also allows us to define custom element types. It runs diffing algorithm on these virtual elements to update the real DOM in a performant way. +Deku consists of 4 modules packaged together for convenience: -**Heads up:** These examples are written using ES2015 syntax. You'll want to make sure you're familiar with [modules](https://babeljs.io/docs/learn-es2015/#modules) and [destructuring](https://babeljs.io/docs/learn-es2015/#destructuring) to follow along. +* `element`: Creating virtual elements +* `diff`: Computing the difference between two virtual elements +* `dom`: DOM renderer +* `string`: HTML string renderer -Virtual elements are plain objects that represent real DOM elements: +### Installation -```js -{ - type: 'button', - attributes: { class: 'Button' }, - children: props.children -} ``` - -Which can then be rendered by Deku to the DOM. This example will render a button to the `document.body` with a class of `Button`: - -```js -import {render,tree} from 'deku' - -var button = { - type: 'button', - attributes: { class: 'Button' } -} - -// Create an app -var app = tree(button) - -// Automatically re-renders the app when state changes -render(app, document.body) +npm install --save deku ``` -You can define your own custom elements that can contain their own state. These are called **components**. Components are objects that (at the very least) have a render function. This render function is passed in a component object with these properties: - -* `props`: This is any external data -* `state`: This is any internal data that is hidden from the world -* `id`: The instance id of the component - -Here's an example `App` component that renders a paragraph: - -```js -import {render,tree} from 'deku' - -// Define our custom element. The render method should -// return a new virtual element. -var App = { - render: function ({ props, state }) { - return { - type: 'p', - attributes: { color: props.color } - } - } -} - -// Then create a virtual element with our custom type -var app = tree({ - type: App, // <- custom type instead of a string - attributes: { color: 'red' } // <- these become 'props' -}) - -// And render it to the DOM -render(app, document.body) -``` - -## Virtual Elements - -But these virtual elements aren't very easy to read. The good news is that you can use other libraries to add a DSL for creating these objects: - -* [virtual-element](https://github.com/dekujs/virtual-element) -* [magic-virtual-element](https://github.com/dekujs/magic-virtual-element) - -So you can use the `virtual-element` module to easily create these objects instead: +We support the latest two versions of each browser. This means we only support IE10+. -```js -element('div', { class: "App" }, [ - element('button', { class: "Button" }, 'Click Me!') -]) -``` - -And if you're using `virtual-element` [you can also use JSX](https://github.com/dekujs/deku/blob/master/docs/guides/jsx.md) to make rendering nodes more developer friendly. This is equivalent to the previous example: - -```jsx -
- -
-``` - -JSX might seem offensive at first, but if you're already using Babel you get JSX for free. Think of it as a more familiar way to define tree structures. **The rest of the examples will assume we're using JSX.** You can go ahead and imagine the same syntax using the `virtual-element` DSL or the raw object format. +[![Sauce Test Status](https://saucelabs.com/browser-matrix/deku.svg)](https://saucelabs.com/u/deku) -So the previous app example would look like this using JSX (notice that we're importing the `virtual-element` module this time): +### Example ```js -import element from 'virtual-element' -import {render,tree} from 'deku' +import {dom, element} from 'deku' +import {createStore} from 'redux' +import reducer from './reducer' -// Define our custom element -var App = { - render: function ({ props }) { - return

Hello World

- } -} - -var app = tree() - -// And render it to the DOM -render(app, document.body) -``` +// Create a Redux store to handle actions and side-effects +let store = createStore(reducer) -## Custom Elements - -So now we can start defining components in their own module and export them. Let's create a custom button element: - -```js -// button.js -import element from 'virtual-element' +// Create a renderer that can turn vnodes into real DOM elements +let render = dom.createRenderer(document.body, store.dispatch) +// Define a state-less component let MyButton = { - render ({props}) { - return - } + render: ({ props, children }) { + return + } } -export {MyButton} -``` - -Then we can import it and render it in the same way: - -```js -// app.js -import element from 'virtual-element' -import {MyButton} from './button' -import {render,tree} from 'deku' - -// We're using our custom MyButton element -var app = tree( -
- Hello World! -
+// Update the page and add redux state to the context +render( + Hello World!, + store.getState() ) - -render(app, document.body) ``` -You can also render these same elements and custom elements on the server using `renderString` instead of `render`: +### Documentation -```js -// server.js -import element from 'virtual-element' -import {MyButton} from './button' -import {renderString,tree} from 'deku' - -let html = renderString(tree( -
- Hello World! -
-)) -``` - -That's all there is to it. Components can also have [hook functions](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) so you can do some work when they are created, removed or updated, and you can [add state](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) to your components. - -## Next steps - -* [Installing](https://github.com/dekujs/deku/blob/master/docs/guides/install.md) -* [Component API](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) -* [Using JSX](https://github.com/dekujs/deku/blob/master/docs/guides/jsx.md) -* [Client + Server Rendering Example](https://github.com/dekujs/todomvc) -* [Community resources](https://github.com/stevenmiller888/awesome-deku) -* [Contributing to Deku](https://github.com/dekujs/deku/blob/master/docs/guides/development.md) - -## Tests - -Deku is built with Browserify. You can run the tests in a browser by running `make test`. Learn how to build and work on Deku [in the documentation](https://github.com/dekujs/deku/blob/master/docs/guides/development.md). - -[![Sauce Test Status](https://saucelabs.com/browser-matrix/deku.svg)](https://saucelabs.com/u/deku) +You can [read the documentation online](https://anthonyshort.gitbooks.io/dekujs/content/) at Gitbook. -## License +### License The MIT License (MIT) Copyright (c) 2015 Anthony Short diff --git a/book.json b/book.json index 9e26dfe..507d0d7 100644 --- a/book.json +++ b/book.json @@ -1 +1,18 @@ -{} \ No newline at end of file +{ + "gitbook": "2.x.x", + "title": "Deku", + "description": "Learn how to use Deku", + "structure": { + "summary": "docs/README.md" + }, + "plugins": ["edit-link", "prism", "-highlight", "github"], + "pluginsConfig": { + "edit-link": { + "base": "https://github.com/dekujs/deku/tree/master", + "label": "Edit This Page" + }, + "github": { + "url": "https://github.com/dekujs/deku/" + } + } +} diff --git a/build/deku.js b/build/deku.js deleted file mode 100644 index e9c413c..0000000 --- a/build/deku.js +++ /dev/null @@ -1,4333 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.deku = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof _require=="function"&&_require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof _require=="function"&&_require;for(var o=0;o 0) { - console.info('deku: The container element is not empty. These elements will be removed. Read more: http://cl.ly/b0Sr') - } - if (container === document.body) { - console.warn('deku: Using document.body is allowed but it can cause some issues. Read more: http://cl.ly/b0SC') - } - removeAllChildren(container) - container.appendChild(currentNativeElement) - } else if (currentElement !== app.element) { - currentNativeElement = patch(rootId, currentElement, app.element, currentNativeElement) - currentElement = app.element - updateChildren(rootId) - } else { - updateChildren(rootId) - } - - // Call mount events on all new entities - flushMountQueue() - - // Allow rendering again. - isRendering = false - } - - /** - * Call hooks for all new entities that have been created in - * the last render from the bottom up. - */ - - function flushMountQueue () { - while (mountQueue.length > 0) { - var entityId = mountQueue.shift() - var entity = entities[entityId] - trigger('afterRender', entity, [entity.context, entity.nativeElement]) - trigger('afterMount', entity, [entity.context, entity.nativeElement, setState(entity)]) - } - } - - /** - * Clear the current scheduled frame - */ - - function clearFrame () { - if (!frameId) return - raf.cancel(frameId) - frameId = 0 - } - - /** - * Update a component. - * - * The entity is just the data object for a component instance. - * - * @param {String} id Component instance id. - */ - - function updateEntity (entityId) { - var entity = entities[entityId] - setSources(entity) - - if (!shouldUpdate(entity)) { - commit(entity) - return updateChildren(entityId) - } - - var currentTree = entity.virtualElement - var nextProps = entity.pendingProps - var nextState = entity.pendingState - var previousState = entity.context.state - var previousProps = entity.context.props - - // hook before rendering. could modify state just before the render occurs. - trigger('beforeUpdate', entity, [entity.context, nextProps, nextState]) - trigger('beforeRender', entity, [entity.context]) - - // commit state and props. - commit(entity) - - // re-render. - var nextTree = renderEntity(entity) - - // if the tree is the same we can just skip this component - // but we should still check the children to see if they're dirty. - // This allows us to memoize the render function of components. - if (nextTree === currentTree) return updateChildren(entityId) - - // apply new virtual tree to native dom. - entity.nativeElement = patch(entityId, currentTree, nextTree, entity.nativeElement) - entity.virtualElement = nextTree - updateChildren(entityId) - - // trigger render hook - trigger('afterRender', entity, [entity.context, entity.nativeElement]) - - // trigger afterUpdate after all children have updated. - trigger('afterUpdate', entity, [entity.context, previousProps, previousState, setState(entity)]) - } - - /** - * Update all the children of an entity. - * - * @param {String} id Component instance id. - */ - - function updateChildren (entityId) { - forEach(children[entityId], function (childId) { - updateEntity(childId) - }) - } - - /** - * Remove all of the child entities of an entity - * - * @param {Entity} entity - */ - - function unmountChildren (entityId) { - forEach(children[entityId], function (childId) { - unmountEntity(childId) - }) - } - - /** - * Remove the root element. If this is called synchronously we need to - * cancel any pending future updates. - */ - - function removeNativeElement () { - clearFrame() - removeElement(rootId, '0', currentNativeElement) - currentNativeElement = null - } - - /** - * Create a native element from a virtual element. - * - * @param {String} entityId - * @param {String} path - * @param {Object} vnode - * - * @return {HTMLDocumentFragment} - */ - - function toNative (entityId, path, vnode) { - switch (nodeType(vnode)) { - case 'text': return toNativeText(vnode) - case 'empty': return toNativeEmptyElement(entityId, path) - case 'element': return toNativeElement(entityId, path, vnode) - case 'component': return toNativeComponent(entityId, path, vnode) - } - } - - /** - * Create a native text element from a virtual element. - * - * @param {Object} vnode - */ - - function toNativeText (text) { - return document.createTextNode(text) - } - - /** - * Create a native element from a virtual element. - */ - - function toNativeElement (entityId, path, vnode) { - var el - var attributes = vnode.attributes - var tagName = vnode.type - var childNodes = vnode.children - - // create element either from pool or fresh. - if (svg.isElement(tagName)) { - el = document.createElementNS(svg.namespace, tagName) - } else { - el = document.createElement(tagName) - } - - // set attributes. - forEach(attributes, function (value, name) { - setAttribute(entityId, path, el, name, value) - }) - - // add children. - forEach(childNodes, function (child, i) { - var childEl = toNative(entityId, path + '.' + i, child) - if (!childEl.parentNode) el.appendChild(childEl) - }) - - // store keys on the native element for fast event handling. - el.__entity__ = entityId - el.__path__ = path - - return el - } - - /** - * Create a native element from a virtual element. - */ - - function toNativeEmptyElement (entityId, path) { - var el = document.createElement('noscript') - el.__entity__ = entityId - el.__path__ = path - return el - } - - /** - * Create a native element from a component. - */ - - function toNativeComponent (entityId, path, vnode) { - var child = new Entity(vnode.type, assign({ children: vnode.children }, vnode.attributes), entityId) - children[entityId][path] = child.id - return mountEntity(child) - } - - /** - * Patch an element with the diff from two trees. - */ - - function patch (entityId, prev, next, el) { - return diffNode('0', entityId, prev, next, el) - } - - /** - * Create a diff between two trees of nodes. - */ - - function diffNode (path, entityId, prev, next, el) { - var leftType = nodeType(prev) - var rightType = nodeType(next) - - // Type changed. This could be from element->text, text->ComponentA, - // ComponentA->ComponentB etc. But NOT div->span. These are the same type - // (ElementNode) but different tag name. - if (leftType !== rightType) return replaceElement(entityId, path, el, next) - - switch (rightType) { - case 'text': return diffText(prev, next, el) - case 'empty': return el - case 'element': return diffElement(path, entityId, prev, next, el) - case 'component': return diffComponent(path, entityId, prev, next, el) - } - } - - /** - * Diff two text nodes and update the element. - */ - - function diffText (previous, current, el) { - if (current !== previous) el.data = current - return el - } - - /** - * Diff the children of an ElementNode. - */ - - function diffChildren (path, entityId, prev, next, el) { - var positions = [] - var hasKeys = false - var childNodes = Array.prototype.slice.apply(el.childNodes) - var leftKeys = reduce(prev.children, keyMapReducer, {}) - var rightKeys = reduce(next.children, keyMapReducer, {}) - var currentChildren = assign({}, children[entityId]) - - function keyMapReducer (acc, child, i) { - if (child && child.attributes && child.attributes.key != null) { - acc[child.attributes.key] = { - element: child, - index: i - } - hasKeys = true - } - return acc - } - - // Diff all of the nodes that have keys. This lets us re-used elements - // instead of overriding them and lets us move them around. - if (hasKeys) { - // Removals - forEach(leftKeys, function (leftNode, key) { - if (rightKeys[key] == null) { - var leftPath = path + '.' + leftNode.index - removeElement( - entityId, - leftPath, - childNodes[leftNode.index] - ) - } - }) - - // Update nodes - forEach(rightKeys, function (rightNode, key) { - var leftNode = leftKeys[key] - - // We only want updates for now - if (leftNode == null) return - - var leftPath = path + '.' + leftNode.index - - // Updated - positions[rightNode.index] = diffNode( - leftPath, - entityId, - leftNode.element, - rightNode.element, - childNodes[leftNode.index] - ) - }) - - // Update the positions of all child components and event handlers - forEach(rightKeys, function (rightNode, key) { - var leftNode = leftKeys[key] - - // We just want elements that have moved around - if (leftNode == null || leftNode.index === rightNode.index) return - - var rightPath = path + '.' + rightNode.index - var leftPath = path + '.' + leftNode.index - - // Update all the child component path positions to match - // the latest positions if they've changed. This is a bit hacky. - forEach(currentChildren, function (childId, childPath) { - if (leftPath === childPath) { - delete children[entityId][childPath] - children[entityId][rightPath] = childId - } - }) - }) - - // Now add all of the new nodes last in case their path - // would have conflicted with one of the previous paths. - forEach(rightKeys, function (rightNode, key) { - var rightPath = path + '.' + rightNode.index - if (leftKeys[key] == null) { - positions[rightNode.index] = toNative( - entityId, - rightPath, - rightNode.element - ) - } - }) - } else { - var maxLength = Math.max(prev.children.length, next.children.length) - // Now diff all of the nodes that don't have keys - for (var i = 0; i < maxLength; i++) { - var leftNode = prev.children[i] - var rightNode = next.children[i] - - // Removals - if (rightNode === undefined) { - removeElement( - entityId, - path + '.' + i, - childNodes[i] - ) - continue - } - - // New Node - if (leftNode === undefined) { - positions[i] = toNative( - entityId, - path + '.' + i, - rightNode - ) - continue - } - - // Updated - positions[i] = diffNode( - path + '.' + i, - entityId, - leftNode, - rightNode, - childNodes[i] - ) - } - } - - // Reposition all the elements - forEach(positions, function (childEl, newPosition) { - var target = el.childNodes[newPosition] - if (childEl && childEl !== target) { - if (target) { - el.insertBefore(childEl, target) - } else { - el.appendChild(childEl) - } - } - }) - } - - /** - * Diff the attributes and add/remove them. - */ - - function diffAttributes (prev, next, el, entityId, path) { - var nextAttrs = next.attributes - var prevAttrs = prev.attributes - - // add new attrs - forEach(nextAttrs, function (value, name) { - if (events[name] || !(name in prevAttrs) || prevAttrs[name] !== value) { - setAttribute(entityId, path, el, name, value) - } - }) - - // remove old attrs - forEach(prevAttrs, function (value, name) { - if (!(name in nextAttrs)) { - removeAttribute(entityId, path, el, name) - } - }) - } - - /** - * Update a component with the props from the next node. If - * the component type has changed, we'll just remove the old one - * and replace it with the new component. - */ - - function diffComponent (path, entityId, prev, next, el) { - if (next.type !== prev.type) { - return replaceElement(entityId, path, el, next) - } else { - var targetId = children[entityId][path] - - // This is a hack for now - if (targetId) { - updateEntityProps(targetId, assign({ children: next.children }, next.attributes)) - } - - return el - } - } - - /** - * Diff two element nodes. - */ - - function diffElement (path, entityId, prev, next, el) { - if (next.type !== prev.type) return replaceElement(entityId, path, el, next) - diffAttributes(prev, next, el, entityId, path) - diffChildren(path, entityId, prev, next, el) - return el - } - - /** - * Removes an element from the DOM and unmounts and components - * that are within that branch - * - * side effects: - * - removes element from the DOM - * - removes internal references - * - * @param {String} entityId - * @param {String} path - * @param {HTMLElement} el - */ - - function removeElement (entityId, path, el) { - var childrenByPath = children[entityId] - var childId = childrenByPath[path] - var entityHandlers = handlers[entityId] || {} - var removals = [] - - // If the path points to a component we should use that - // components element instead, because it might have moved it. - if (childId) { - var child = entities[childId] - el = child.nativeElement - unmountEntity(childId) - removals.push(path) - } else { - // Just remove the text node - if (!isElement(el)) return el && el.parentNode.removeChild(el) - // Then we need to find any components within this - // branch and unmount them. - forEach(childrenByPath, function (childId, childPath) { - if (childPath === path || isWithinPath(path, childPath)) { - unmountEntity(childId) - removals.push(childPath) - } - }) - - // Remove all events at this path or below it - forEach(entityHandlers, function (fn, handlerPath) { - if (handlerPath === path || isWithinPath(path, handlerPath)) { - removeEvent(entityId, handlerPath) - } - }) - } - - // Remove the paths from the object without touching the - // old object. This keeps the object using fast properties. - forEach(removals, function (path) { - delete children[entityId][path] - }) - - // Remove it from the DOM - el.parentNode.removeChild(el) - } - - /** - * Replace an element in the DOM. Removing all components - * within that element and re-rendering the new virtual node. - * - * @param {Entity} entity - * @param {String} path - * @param {HTMLElement} el - * @param {Object} vnode - * - * @return {void} - */ - - function replaceElement (entityId, path, el, vnode) { - var parent = el.parentNode - var index = Array.prototype.indexOf.call(parent.childNodes, el) - - // remove the previous element and all nested components. This - // needs to happen before we create the new element so we don't - // get clashes on the component paths. - removeElement(entityId, path, el) - - // then add the new element in there - var newEl = toNative(entityId, path, vnode) - var target = parent.childNodes[index] - - if (target) { - parent.insertBefore(newEl, target) - } else { - parent.appendChild(newEl) - } - - // walk up the tree and update all `entity.nativeElement` references. - if (entityId !== 'root' && path === '0') { - updateNativeElement(entityId, newEl) - } - - return newEl - } - - /** - * Update all entities in a branch that have the same nativeElement. This - * happens when a component has another component as it's root node. - * - * @param {String} entityId - * @param {HTMLElement} newEl - * - * @return {void} - */ - - function updateNativeElement (entityId, newEl) { - var target = entities[entityId] - if (target.ownerId === 'root') return - if (children[target.ownerId]['0'] === entityId) { - entities[target.ownerId].nativeElement = newEl - updateNativeElement(target.ownerId, newEl) - } - } - - /** - * Set the attribute of an element, performing additional transformations - * dependning on the attribute name - * - * @param {HTMLElement} el - * @param {String} name - * @param {String} value - */ - - function setAttribute (entityId, path, el, name, value) { - if (!value && typeof value !== 'number') { - removeAttribute(entityId, path, el, name) - return - } - if (events[name]) { - addEvent(entityId, path, events[name], value) - return - } - switch (name) { - case 'checked': - case 'disabled': - case 'selected': - el[name] = true - break - case 'innerHTML': - el.innerHTML = value - break - case 'value': - setElementValue(el, value) - break - case svg.isAttribute(name): - el.setAttributeNS(svg.namespace, name, value) - break - default: - el.setAttribute(name, value) - break - } - } - - /** - * Remove an attribute, performing additional transformations - * dependning on the attribute name - * - * @param {HTMLElement} el - * @param {String} name - */ - - function removeAttribute (entityId, path, el, name) { - if (events[name]) { - removeEvent(entityId, path, events[name]) - return - } - switch (name) { - case 'checked': - case 'disabled': - case 'selected': - el[name] = false - break - case 'innerHTML': - el.innerHTML = '' - /* falls through */ - case 'value': - setElementValue(el, null) - break - default: - el.removeAttribute(name) - break - } - } - - /** - * Checks to see if one tree path is within - * another tree path. Example: - * - * 0.1 vs 0.1.1 = true - * 0.2 vs 0.3.5 = false - * - * @param {String} target - * @param {String} path - * - * @return {Boolean} - */ - - function isWithinPath (target, path) { - return path.indexOf(target + '.') === 0 - } - - /** - * Is the DOM node an element node - * - * @param {HTMLElement} el - * - * @return {Boolean} - */ - - function isElement (el) { - return !!(el && el.tagName) - } - - /** - * Remove all the child nodes from an element - * - * @param {HTMLElement} el - */ - - function removeAllChildren (el) { - while (el.firstChild) el.removeChild(el.firstChild) - } - - /** - * Trigger a hook on a component. - * - * @param {String} name Name of hook. - * @param {Entity} entity The component instance. - * @param {Array} args To pass along to hook. - */ - - function trigger (name, entity, args) { - if (typeof entity.component[name] !== 'function') return - return entity.component[name].apply(null, args) - } - - /** - * Update an entity to match the latest rendered vode. We always - * replace the props on the component when composing them. This - * will trigger a re-render on all children below this point. - * - * @param {Entity} entity - * @param {String} path - * @param {Object} vnode - * - * @return {void} - */ - - function updateEntityProps (entityId, nextProps) { - var entity = entities[entityId] - entity.pendingProps = defaults({}, nextProps, entity.component.defaultProps || {}) - entity.dirty = true - invalidate() - } - - /** - * Update component instance state. - */ - - function updateEntityState (entity, nextState) { - entity.pendingState = assign(entity.pendingState, nextState) - entity.dirty = true - invalidate() - } - - /** - * Commit props and state changes to an entity. - */ - - function commit (entity) { - entity.context = { - state: entity.pendingState, - props: entity.pendingProps, - id: entity.id - } - entity.pendingState = assign({}, entity.context.state) - entity.pendingProps = assign({}, entity.context.props) - entity.dirty = false - if (typeof entity.component.validate === 'function') { - entity.component.validate(entity.context) - } - } - - /** - * Try to avoid creating new virtual dom if possible. - * - * Later we may expose this so you can override, but not there yet. - */ - - function shouldUpdate (entity) { - if (!entity.dirty) return false - if (!entity.component.shouldUpdate) return true - var nextProps = entity.pendingProps - var nextState = entity.pendingState - var bool = entity.component.shouldUpdate(entity.context, nextProps, nextState) - return bool - } - - /** - * Register an entity. - * - * This is mostly to pre-preprocess component properties and values chains. - * - * The end result is for every component that gets mounted, - * you create a set of IO nodes in the network from the `value` definitions. - * - * @param {Component} component - */ - - function register (entity) { - registerEntity(entity) - var component = entity.component - if (component.registered) return - - // initialize sources once for a component type. - registerSources(entity) - component.registered = true - } - - /** - * Add entity to data-structures related to components/entities. - * - * @param {Entity} entity - */ - - function registerEntity (entity) { - var component = entity.component - // all entities for this component type. - var entities = component.entities = component.entities || {} - // add entity to component list - entities[entity.id] = entity - // map to component so you can remove later. - components[entity.id] = component - } - - /** - * Initialize sources for a component by type. - * - * @param {Entity} entity - */ - - function registerSources (entity) { - var component = components[entity.id] - // get 'class-level' sources. - // if we've already hooked it up, then we're good. - var sources = component.sources - if (sources) return - var entities = component.entities - - // hook up sources. - var map = component.sourceToPropertyName = {} - component.sources = sources = [] - var propTypes = component.propTypes - for (var name in propTypes) { - var data = propTypes[name] - if (!data) continue - if (!data.source) continue - sources.push(data.source) - map[data.source] = name - } - - // send value updates to all component instances. - sources.forEach(function (source) { - connections[source] = connections[source] || [] - connections[source].push(update) - - function update (data) { - var prop = map[source] - for (var entityId in entities) { - var entity = entities[entityId] - var changes = {} - changes[prop] = data - updateEntityProps(entityId, assign(entity.pendingProps, changes)) - } - } - }) - } - - /** - * Set the initial source value on the entity - * - * @param {Entity} entity - */ - - function setSources (entity) { - var component = entity.component - var map = component.sourceToPropertyName - var sources = component.sources - sources.forEach(function (source) { - var name = map[source] - if (entity.pendingProps[name] != null) return - entity.pendingProps[name] = app.sources[source] // get latest value plugged into global store - }) - } - - /** - * Add all of the DOM event listeners - */ - - function addNativeEventListeners () { - forEach(events, function (eventType) { - rootElement.addEventListener(eventType, handleEvent, true) - }) - } - - /** - * Add all of the DOM event listeners - */ - - function removeNativeEventListeners () { - forEach(events, function (eventType) { - rootElement.removeEventListener(eventType, handleEvent, true) - }) - } - - /** - * Handle an event that has occured within the container - * - * @param {Event} event - */ - - function handleEvent (event) { - var target = event.target - var eventType = event.type - - // Walk up the DOM tree and see if there is a handler - // for this event type higher up. - while (target) { - var fn = keypath.get(handlers, [target.__entity__, target.__path__, eventType]) - if (fn) { - event.delegateTarget = target - if (fn(event) === false) break - } - target = target.parentNode - } - } - - /** - * Bind events for an element, and all it's rendered child elements. - * - * @param {String} path - * @param {String} event - * @param {Function} fn - */ - - function addEvent (entityId, path, eventType, fn) { - keypath.set(handlers, [entityId, path, eventType], function (e) { - var entity = entities[entityId] - if (entity) { - return fn(e, entity.context, setState(entity)) - } else { - return fn(e) - } - }) - } - - /** - * Unbind events for a entityId - * - * @param {String} entityId - */ - - function removeEvent (entityId, path, eventType) { - var args = [entityId] - if (path) args.push(path) - if (eventType) args.push(eventType) - keypath.del(handlers, args) - } - - /** - * Unbind all events from an entity - * - * @param {Entity} entity - */ - - function removeAllEvents (entityId) { - keypath.del(handlers, [entityId]) - } - - /** - * Used for debugging to inspect the current state without - * us needing to explicitly manage storing/updating references. - * - * @return {Object} - */ - - function inspect () { - return { - entities: entities, - handlers: handlers, - connections: connections, - currentElement: currentElement, - options: options, - app: app, - container: container, - children: children - } - } - - /** - * Return an object that lets us completely remove the automatic - * DOM rendering and export debugging tools. - */ - - return { - remove: teardown, - inspect: inspect - } -} - -/** - * A rendered component instance. - * - * This manages the lifecycle, props and state of the component. - * It's basically just a data object for more straightfoward lookup. - * - * @param {Component} component - * @param {Object} props - */ - -function Entity (component, props, ownerId) { - this.id = uid() - this.ownerId = ownerId - this.component = component - this.propTypes = component.propTypes || {} - this.context = {} - this.context.id = this.id - this.context.props = defaults(props || {}, component.defaultProps || {}) - this.context.state = this.component.initialState ? this.component.initialState(this.context.props) : {} - this.pendingProps = assign({}, this.context.props) - this.pendingState = assign({}, this.context.state) - this.dirty = false - this.virtualElement = null - this.nativeElement = null - this.displayName = component.name || 'Component' -} - -/** - * Retrieve the nearest 'body' ancestor of the given element or else the root - * element of the document in which stands the given element. - * - * This is necessary if you want to attach the events handler to the correct - * element and be able to dispatch events in document fragments such as - * Shadow DOM. - * - * @param {HTMLElement} el The element on which we will render an app. - * @return {HTMLElement} The root element on which we will attach the events - * handler. - */ - -function getRootElement (el) { - while (el.parentElement) { - if (el.tagName === 'BODY' || !el.parentElement) { - return el - } - el = el.parentElement - } - return el -} - -/** - * Set the value property of an element and keep the text selection - * for input fields. - * - * @param {HTMLElement} el - * @param {String} value - */ - -function setElementValue (el, value) { - if (el === document.activeElement && canSelectText(el)) { - var start = el.selectionStart - var end = el.selectionEnd - el.value = value - el.setSelectionRange(start, end) - } else { - el.value = value - } -} - -/** - * For some reason only certain types of inputs can set the selection range. - * - * @param {HTMLElement} el - * - * @return {Boolean} - */ - -function canSelectText (el) { - return el.tagName === 'INPUT' && ['text', 'search', 'password', 'tel', 'url'].indexOf(el.type) > -1 -} - -},{"./events":2,"./node-type":4,"./svg":7,"component-raf":13,"fast.js/forEach":17,"fast.js/object/assign":20,"fast.js/reduce":23,"get-uid":24,"is-dom":25,"object-defaults":28,"object-path":29}],6:[function(_require,module,exports){ -var defaults = _require('object-defaults') -var nodeType = _require('./node-type') -var type = _require('component-type') - -/** - * Expose `stringify`. - */ - -module.exports = function (app) { - if (!app.element) { - throw new Error('No element mounted') - } - - /** - * Render to string. - * - * @param {Component} component - * @param {Object} [props] - * @return {String} - */ - - function stringify (component, optProps, children) { - var propTypes = component.propTypes || {} - var props = defaults(optProps || {}, component.defaultProps || {}) - var state = component.initialState ? component.initialState(props) : {} - props.children = children - - for (var name in propTypes) { - var options = propTypes[name] - if (options.source) { - props[name] = app.sources[options.source] - } - } - - if (component.beforeMount) component.beforeMount({ props: props, state: state }) - if (component.beforeRender) component.beforeRender({ props: props, state: state }) - var node = component.render({ props: props, state: state }) - return stringifyNode(node, '0') - } - - /** - * Render a node to a string - * - * @param {Node} node - * @param {Tree} tree - * - * @return {String} - */ - - function stringifyNode (node, path) { - switch (nodeType(node)) { - case 'empty': return '