├── .npmignore
├── .gitignore
├── src
├── id.js
├── index.js
├── element.js
├── build.js
├── dom.js
├── string.js
├── fix_props.js
└── widget.js
├── support
└── api_header.md
├── docs
├── README.md
├── docpress.json
├── examples.md
├── about-deku.md
├── cheatsheet.md
├── jsx.md
├── components.md
└── api.md
├── .eslintrc
├── test
├── support
│ ├── r.js
│ └── tapedom.js
├── style_test.js
├── index.js
├── fix_props_test.js
├── path_test.js
├── dispatch_test.js
├── children_test.js
├── deku
│ └── create_dom_renderer_test.js
├── basic_test.js
└── string_test.js
├── .travis.yml
├── README.md
├── HISTORY.md
└── package.json
/.npmignore:
--------------------------------------------------------------------------------
1 | /src
2 | /test
3 | /docs
4 | /_docpress
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /lib
3 | /_docpress
4 | /yarn.lock
5 |
--------------------------------------------------------------------------------
/src/id.js:
--------------------------------------------------------------------------------
1 | var id = 1
2 |
3 | export default function getId () {
4 | return 'c' + (id++)
5 | }
6 |
--------------------------------------------------------------------------------
/support/api_header.md:
--------------------------------------------------------------------------------
1 | # API reference
2 |
3 | ```js
4 | import { dom, element, string } from 'decca'
5 | ```
6 |
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { createRenderer } from './dom'
2 | import createElement from './element'
3 | import { render } from './string'
4 |
5 | export const dom = { createRenderer }
6 | export const element = createElement
7 | export const string = { render }
8 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [decca](../README.md)
4 | * Documentation
5 | * [Components](components.md)
6 | * [API reference](api.md)
7 | * [JSX](jsx.md)
8 | * [Examples](examples.md)
9 | * [About Deku](about-deku.md)
10 | * [Cheatsheet](cheatsheet.md)
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | extends: ['standard', 'standard-jsx'],
3 | rules: {
4 | 'react/prop-types': 0,
5 | 'react/react-in-jsx-scope': 0,
6 | 'react/no-unknown-property': 0,
7 | 'no-unused-vars': [2, { vars: 'all', args: 'none', varsIgnorePattern: '^element$' }]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/support/r.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Helper
3 | */
4 |
5 | import { dom } from '../../src'
6 |
7 | module.exports = function r (...args) {
8 | const div = document.createElement('div')
9 | const render = dom.createRenderer(div)
10 | render(...args)
11 | return { div, render }
12 | }
13 |
--------------------------------------------------------------------------------
/test/style_test.js:
--------------------------------------------------------------------------------
1 | import { element as h } from '../src'
2 | import test from 'tape'
3 | import r from './support/r'
4 |
5 | test('string styles', (t) => {
6 | const { div } = r(h('div', { style: 'color: blue' }, 'hello'))
7 | t.ok(/^
hello<\/div>$/.test(div.innerHTML), 'renders')
8 | t.end()
9 | })
10 |
--------------------------------------------------------------------------------
/docs/docpress.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": "rstacruz/decca",
3 | "css": [
4 | "http://ricostacruz.com/docpress-rsc/style.css"
5 | ],
6 | "markdown": {
7 | "typographer": true,
8 | "plugins": {
9 | "decorate": {}
10 | }
11 | },
12 | "googleAnalytics": {
13 | "id": "UA-20473929-1",
14 | "domain": "ricostacruz.com"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | require('jsdom-global')()
2 | require('./support/tapedom')
3 | require('./basic_test')
4 | require('./children_test')
5 | require('./dispatch_test')
6 | require('./path_test')
7 | require('./string_test')
8 | require('./style_test')
9 | require('./fix_props_test')
10 | require('./deku/create_dom_renderer_test')
11 | require('tape')('eslint', require('eslint-engine/tape')())
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | script:
5 | - npm test
6 | - ./node_modules/.bin/docpress build
7 | after_success:
8 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then ./node_modules/.bin/git-update-ghpages -e; fi
9 | cache:
10 | directories:
11 | - node_modules
12 | env:
13 | global:
14 | - GIT_NAME: Travis CI
15 | - GIT_EMAIL: nobody@nobody.org
16 | - GITHUB_REPO: rstacruz/decca
17 | - GIT_SOURCE: _docpress
18 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | - [State example](https://jsfiddle.net/rstacruz/m6mkac75/) — simple example on how to use `state` and `setState` using [deku-stateful].
4 | - [Nesting example](https://jsfiddle.net/rstacruz/azLhvhe2/) — simple example of using components inside a component.
5 | - [Redux example](https://jsfiddle.net/rstacruz/aa2o1sbz/1/) — simple example showing how to integrate [Redux].
6 |
7 | [Redux]: http://redux.js.org/
8 | [deku-stateful]: https://www.npmjs.com/package/deku-stateful
9 |
--------------------------------------------------------------------------------
/test/fix_props_test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import fixProps from '../src/fix_props'
3 |
4 | test('fixProps', (t) => {
5 | t.deepEqual(fixProps({ for: 'foo' }), { htmlFor: 'foo' })
6 | t.deepEqual(
7 | fixProps({ style: 'width: 100px;', attributes: { value: 'one' } }),
8 | { attributes: { style: 'width: 100px;', value: 'one' }, style: 'width: 100px;' }
9 | )
10 | t.deepEqual(fixProps({ class: 'foo' }), { className: 'foo' })
11 | t.deepEqual(fixProps({ onClick: 'foo' }), { onclick: 'foo', onClick: undefined })
12 | t.end()
13 | })
14 |
--------------------------------------------------------------------------------
/test/path_test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | /* @jsx element */
3 |
4 | import { element } from '../src'
5 | import test from 'tape'
6 | import r from './support/r'
7 |
8 | test('path onUpdate', (t) => {
9 | t.plan(2)
10 |
11 | var lastPath
12 | const App = {
13 | onCreate ({ path }) { lastPath = path },
14 | onUpdate ({ path }) {
15 | t.equal(path, lastPath, 'path is the same onCreate and onUpdate')
16 | t.ok(path, 'has a path onUpdate')
17 | },
18 | render ({ context }) { return
}
19 | }
20 |
21 | const { div, render } = r(
)
22 | render(
)
23 | t.end()
24 | })
25 |
--------------------------------------------------------------------------------
/docs/about-deku.md:
--------------------------------------------------------------------------------
1 | # About Deku
2 |
3 | Decca is an implementation of [Deku] in <200 lines using [virtual-dom]. The full Deku v2 API is implemented (plus a little more)—you can use Decca while Deku v2.0.0 is in development.
4 |
5 | ## Differences with Deku
6 |
7 | - `path` (see [Components](components.md)) in Deku is a dot-separated path of the component relative to its ancestors (eg, `aw398.0.0`). In Decca, `path` is simply a unique ID for the component instance (eg, `c83`). It fulfills the same function as a unique identifier.
8 |
9 | [Deku]: https://dekujs.github.io/deku
10 | [virtual-dom]: https://www.npmjs.com/package/virtual-dom
11 | [lifecycle hooks]: components.md
12 |
--------------------------------------------------------------------------------
/test/dispatch_test.js:
--------------------------------------------------------------------------------
1 | import { element as h, dom } from '../src'
2 | import test from 'tape'
3 |
4 | test('dispatch', (t) => {
5 | const Button = {
6 | render ({ props, dispatch }) {
7 | t.equal(dispatch, 'CTX')
8 | return h('button', {}, props.label)
9 | }
10 | }
11 |
12 | const App = {
13 | render ({ dispatch }) {
14 | t.equal(dispatch, 'CTX')
15 | return h('div', {}, 'hi. ', h(Button, { label: 'press' }))
16 | }
17 | }
18 |
19 | const div = document.createElement('div')
20 | const render = dom.createRenderer(div, 'CTX')
21 | render(h(App))
22 | t.equal(div.innerHTML, '
hi.
')
23 | t.end()
24 | })
25 |
--------------------------------------------------------------------------------
/test/children_test.js:
--------------------------------------------------------------------------------
1 | import { element } from '../src'
2 | import test from 'tape'
3 | import r from './support/r'
4 |
5 | test('children', (t) => {
6 | const App = {
7 | render ({ props, children }) {
8 | return
{children}
9 | }
10 | }
11 | const { div } = r(
hi)
12 | t.equal(div.innerHTML, '
hi
')
13 | t.end()
14 | })
15 |
16 | test('text with children', (t) => {
17 | const App = {
18 | render ({ props, children }) {
19 | return
hi {children}
20 | }
21 | }
22 | const { div } = r(
John)
23 | t.equal(div.innerHTML, '
hi John
')
24 | t.end()
25 | })
26 |
27 |
--------------------------------------------------------------------------------
/src/element.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @module decca/element
3 | */
4 |
5 | /**
6 | * Returns a vnode (*Element*) to be consumed by [render()](#render).
7 | * This is compatible with JSX.
8 | *
9 | * @param {string} tag Tag name (eg, `'div'`)
10 | * @param {object} props Properties
11 | * @param {...(Element | string)=} children Children
12 | * @return {Element} An element
13 | */
14 |
15 | function element (tag, props, ...children) {
16 | return { tag, props, children }
17 | }
18 |
19 | export default element
20 |
21 | /**
22 | * A vnode (*Element*) to be consumed by [render()](#render).
23 | * This is generated via [element()](#element).
24 | *
25 | * @typedef Element
26 | * @property {string} tag Tag name (eg, `'div'`)
27 | * @property {object} props Properties
28 | * @property {Array
} children Children
29 | */
30 |
--------------------------------------------------------------------------------
/src/build.js:
--------------------------------------------------------------------------------
1 | const h = require('virtual-dom/h')
2 | import fixProps from './fix_props'
3 | import Widget from './widget'
4 |
5 | /*
6 | * A rendering pass.
7 | * This closure is responsible for:
8 | *
9 | * - keeping aware of `context` to be passed down to Components
10 | *
11 | * build = buildPass(...)
12 | * build(el) // render a component/node
13 | */
14 |
15 | export default function buildPass (context, dispatch) {
16 | /*
17 | * Builds from a vnode (`element()` output) to a virtual hyperscript element.
18 | * The `context` and `dispatch` is passed down recursively.
19 | * https://github.com/Matt-Esch/virtual-dom/blob/master/virtual-hyperscript/README.md
20 | */
21 |
22 | return function build (el) {
23 | if (typeof el === 'string') return el
24 | if (typeof el === 'number') return '' + el
25 | if (typeof el === 'undefined' || el === null) return
26 | if (Array.isArray(el)) return el.map(build)
27 |
28 | const { tag, props, children } = el
29 |
30 | if (typeof tag === 'object') {
31 | // Defer to Widget if it's a component
32 | if (!tag.render) throw new Error('no render() in component')
33 | return new Widget(
34 | { component: tag, props, children },
35 | { context, dispatch },
36 | build)
37 | } else if (typeof tag === 'function') {
38 | // Pure components
39 | return new Widget(
40 | { component: { render: tag }, props, children },
41 | { context, dispatch },
42 | build)
43 | }
44 |
45 | return h(tag, fixProps(props), children.map(build))
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/support/tapedom.js:
--------------------------------------------------------------------------------
1 | if (typeof window === 'object' && navigator.userAgent.indexOf('Node.js') === -1) {
2 | var tapeDom = require('tape-dom')
3 | var style = document.createElement('style')
4 | style.innerHTML = `
5 | #tests {
6 | font-family: lato, helvetica, sans-serif;
7 | font-size: 14px;
8 | line-height: 1.6;
9 | text-rendering: optimizeLegibility;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | color: #555;
13 | padding: 32px; }
14 | .test {
15 | margin-bottom: 16px; }
16 | .test p {
17 | margin: 0; }
18 | .test > .name {
19 | color: #111;
20 | font-weight: bold; }
21 | .test > .assert {
22 | padding-left: 16px; }
23 | .test > .assert > .ok {
24 | color: green;
25 | font-size: .72em;
26 | font-weight: bold;
27 | margin-right: 8px; }
28 | .test > .assert.fail {
29 | background: rgba(250, 200, 200, 0.3); }
30 | .test > .assert.fail > .ok {
31 | background: red;
32 | border-radius: 2px;
33 | padding: 1px 2px 0 2px;
34 | color: white; }
35 | .test > .assert > .actual,
36 | .test > .assert > .expected {
37 | font-family: menlo, consolas, monospace;
38 | font-size: .85em; }
39 | .test > .assert > .actual {
40 | display: block;
41 | color: #daa; }
42 | .test > .assert > .expected {
43 | display: block;
44 | color: #ada; }
45 | .diff {
46 | display: none; }
47 | .diff del {
48 | color: #ddd;
49 | font-family: menlo, consolas, monospace;
50 | font-size: .85em;
51 | color: red; }
52 | `
53 |
54 | document.body.appendChild(style)
55 | tapeDom.stream(require('tape'))
56 | }
57 |
--------------------------------------------------------------------------------
/docs/cheatsheet.md:
--------------------------------------------------------------------------------
1 | # Cheatsheet
2 |
3 | Here's a basic "Hello World" example.
4 |
5 | ```js
6 | /* @jsx element */
7 | import { dom, element } from 'decca'
8 |
9 | const Message = {
10 | render ({ props }) {
11 | return Hello there, {props.name}
12 | }
13 | }
14 |
15 | // Render the app tree
16 | const render = dom.createRenderer(document.body)
17 | render()
18 | ```
19 |
20 | ## Redux
21 |
22 | Pass `store.dispatch` to *createRenderer()*, and `store.getState()` to *render()*.
23 |
24 | ```js
25 | import { createStore } from 'redux'
26 |
27 | const store = createStore(/*...*/)
28 | const render = dom.createRenderer(document.body, store.dispatch)
29 |
30 | function update () {
31 | render(, store.getState())
32 | }
33 |
34 | stoer.subscribe(update)
35 | update()
36 | ```
37 |
38 | ## Components
39 |
40 | Components at least have a `render()` function.
41 |
42 | ```js
43 | import { element } from 'decca'
44 |
45 | exports.render = function ({props, children, context, dispatch, path}) {
46 | /* no-jsx */
47 | return element('div', {class: 'hello'}, 'hello there')
48 |
49 | /* jsx */
50 | return hello there
51 | }
52 | ```
53 |
54 | The model has:
55 |
56 | * `children` - children passed onto component
57 | * `props` - properties passed onto component
58 | * `path` - unique ID of component instance
59 | * `context` - taken from *render()*
60 | * `dispatch` - taken from *createRenderer()*
61 |
62 | ## Component lifecycle
63 |
64 | The following hooks are supported:
65 |
66 | ```js
67 | exports.onCreate = (model) => { ... } // on first create
68 | exports.onUpdate = (model) => { ... } // on every render
69 | exports.onRemove = (model) => { ... } // after removal
70 | ```
71 |
--------------------------------------------------------------------------------
/src/dom.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @module decca/dom
3 | */
4 |
5 | const diff = require('virtual-dom/diff')
6 | const patch = require('virtual-dom/patch')
7 | const createElement = require('virtual-dom/create-element')
8 | import buildPass from './build'
9 |
10 | /**
11 | * Creates a renderer function that will update the given `rootEl` DOM Node if
12 | * called.
13 | *
14 | * @param {DOMNode} el The DOM element to mount to
15 | * @param {function=} dispatch The dispatch function to the store
16 | * @return {render} a renderer function; see [render](#render)
17 | */
18 |
19 | export function createRenderer (rootEl, dispatch) {
20 | var tree, rootNode // virtual-dom states
21 | return render
22 |
23 | /*
24 | * Renders an element `el` (output of `element()`) with the given `context`
25 | */
26 |
27 | function render (el, context) {
28 | var build = buildPass(context, dispatch)
29 | update(build, el) // Update DOM
30 | }
31 |
32 | /*
33 | * Internal: Updates the DOM tree with the given element `el`.
34 | * Either builds the initial tree, or makes a patch on the existing tree.
35 | */
36 |
37 | function update (build, el) {
38 | if (!tree) {
39 | // Build initial tree
40 | tree = build(el)
41 | rootNode = createElement(tree)
42 | rootEl.innerHTML = ''
43 | rootEl.appendChild(rootNode)
44 | } else {
45 | // Build diff
46 | var newTree = build(el)
47 | var delta = diff(tree, newTree)
48 | rootNode = patch(rootNode, delta)
49 | tree = newTree
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * A renderer function returned by [createRenderer()](#createrenderer).
56 | *
57 | * @callback render
58 | * @param {Element} element Virtual element to render; given by [element()](#element)
59 | * @param {*=} context The context to be passed onto the components as `context`
60 | */
61 |
--------------------------------------------------------------------------------
/src/string.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @module decca/string
3 | */
4 |
5 | import getId from './id'
6 |
7 | const assign = require('object-assign')
8 |
9 | /**
10 | * Renders an element into a string without using the DOM.
11 | *
12 | * @param {Element} el The Element to render
13 | * @param {*=} context The context to be passed onto components
14 | * @returns {string} the rendered HTML string
15 | */
16 |
17 | export function render (el, context) {
18 | if (typeof el === 'string') return el
19 | if (typeof el === 'number') return '' + el
20 | if (Array.isArray(el)) return el.map((_el) => render(_el, context))
21 | if (typeof el === 'undefined' || el === null) return ''
22 |
23 | const { tag, props, children } = el
24 |
25 | if (typeof tag === 'string') {
26 | const open = '<' + tag + toProps(props) + '>'
27 | const close = '' + tag + '>'
28 | return open +
29 | (children || []).map((_el) => render(_el, context)).join('') +
30 | close
31 | }
32 |
33 | if (typeof tag === 'object') {
34 | if (!tag.render) throw new Error('component has no render()')
35 | return render(
36 | tag.render({ props: assign({}, props, { children }), path: getId(), context }),
37 | context)
38 | }
39 |
40 | if (typeof tag === 'function') {
41 | // Pure components
42 | return render(
43 | tag({ props: assign({}, props, { children }), path: getId(), context }),
44 | context)
45 | }
46 | }
47 |
48 | /*
49 | * { class: 'foo', id: 'box' } => ' class="foo" id="box"'
50 | */
51 |
52 | function toProps (props) {
53 | if (!props) return ''
54 | var result = []
55 |
56 | Object.keys(props).forEach((attr) => {
57 | if (/^on[A-Za-z]/.test(attr)) return
58 | const val = props[attr]
59 | if (typeof val === 'undefined' || val === null) return
60 | result.push(`${attr}=${JSON.stringify(val)}`)
61 | })
62 |
63 | return result.length ? ' ' + result.join(' ') : ''
64 | }
65 |
--------------------------------------------------------------------------------
/src/fix_props.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Fixes props for virtual-dom's consumption
3 | */
4 |
5 | const assign = require('object-assign')
6 |
7 | // Taken from: https://github.com/wayfair/tungstenjs/blob/42535b17e4894e866abf5711be2266458bc4d508/src/template/template_to_vdom.js#L118-L140
8 |
9 | var transforms = {
10 | // transformed name
11 | 'class': 'className',
12 | 'for': 'htmlFor',
13 | 'http-equiv': 'httpEquiv',
14 | // case specificity
15 | 'accesskey': 'accessKey',
16 | 'autocomplete': 'autoComplete',
17 | 'autoplay': 'autoPlay',
18 | 'colspan': 'colSpan',
19 | 'contenteditable': 'contentEditable',
20 | 'contextmenu': 'contextMenu',
21 | 'enctype': 'encType',
22 | 'formnovalidate': 'formNoValidate',
23 | 'hreflang': 'hrefLang',
24 | 'novalidate': 'noValidate',
25 | 'readonly': 'readOnly',
26 | 'rowspan': 'rowSpan',
27 | 'spellcheck ': 'spellCheck',
28 | 'srcdoc': 'srcDoc',
29 | 'srcset': 'srcSet',
30 | 'tabindex': 'tabIndex'
31 | }
32 |
33 | function transformProperties (props) {
34 | let attrs = {}
35 |
36 | for (let attr in props) {
37 | let transform = transforms[attr] || attr
38 |
39 | attrs[transform] = props[attr]
40 | }
41 |
42 | return attrs
43 | }
44 |
45 | export default function fixProps (props) {
46 | if (props) {
47 | props = transformProperties(props)
48 |
49 | // See https://github.com/Matt-Esch/virtual-dom/blob/master/docs/vnode.md#propertiesstyle-vs-propertiesattributesstyle
50 | if (typeof props.style === 'string') {
51 | props = assign({}, props, {
52 | attributes: assign({}, props.attributes || {}, { style: props.style })
53 | })
54 | }
55 |
56 | // onClick => onclick
57 | Object.keys(props).forEach((key) => {
58 | let m = key.match(/^on([A-Z][a-z]+)$/)
59 | if (m) {
60 | props = assign({}, props, {
61 | [key]: undefined,
62 | [key.toLowerCase()]: props[key]
63 | })
64 | }
65 | })
66 | }
67 |
68 | return props
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/docs/jsx.md:
--------------------------------------------------------------------------------
1 | # JSX
2 |
3 | The `element` function used to create virtual elements is compatible with [JSX] through [Babel]. When using Babel you just need to set the `jsxPragma`. This allows you to automatically transform HTML in your code into `element` calls.
4 |
5 | [JSX]: https://facebook.github.io/jsx/
6 | [Babel]: https://babeljs.io
7 |
8 | ## Setting up Babel
9 |
10 | Be sure to install [babel-preset-es2015](http://babeljs.io/docs/plugins/preset-es2015/) and [babel-plugin-transform-react-jsx](https://babeljs.io/docs/plugins/transform-react-jsx/).
11 |
12 | ```
13 | npm install babel-preset-es2015
14 | npm install babel-plugin-transform-react-jsx
15 | ```
16 |
17 | You will need to set the `pragma`. Here's an example `.babelrc` that sets the pragma:
18 |
19 | ```json
20 | {
21 | "presets": ["es2015"],
22 | "plugins": [
23 | ["transform-react-jsx", {"pragma": "element"}]
24 | ]
25 | }
26 | ```
27 |
28 | Alternatively, you can specify it in each of your files with a `@jsx` pragma comment:
29 |
30 | ```js
31 | /* @jsx element */
32 |
33 | import { element } from 'deku'
34 | ```
35 |
36 | From here, you can use [browserify] with [babelify] to build your final .js package.
37 |
38 | ```sh
39 | npm install --save browserify
40 | npm install --save babelify
41 |
42 | # compile the entry point `index.js` to `dist/application.js`
43 | browserify -t babelify index.js -o dist/application.js
44 | ```
45 |
46 | Alternatively, you can also use [babel-cli] to compile your JS files.
47 |
48 | [browserify]: https://www.npmjs.com/package/browserify
49 | [babelify]: https://www.npmjs.com/package/babelify
50 | [babel-cli]: https://www.npmjs.com/package/babel-cli
51 |
52 | ## How does it work?
53 |
54 | JSX is just syntax sugar for creating object trees. You can use Deku perfectly fine without JSX, but if you're already using Babel you can use JSX without any extra work.
55 |
56 | Instead of writing each `element` call:
57 |
58 | ```js
59 | function render (model) {
60 | return (
61 | element('button', { class: 'Button' }, [
62 | element('span', { class: 'Button-text' }, ['Click Me!'])
63 | ])
64 | )
65 | }
66 | ```
67 |
68 | You can write HTML:
69 |
70 | ```js
71 | function render (model) {
72 | return (
73 |
76 | )
77 | }
78 | ```
79 |
80 | ## References
81 |
82 | *This page is taken from [Deku's documentation](https://github.com/anthonyshort/deku/blob/master/docs/basics/JSX.md).*
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # decca
2 |
3 |
4 |
5 | > Render UI via pure functions and virtual DOM
6 |
7 | Decca allows you to compose DOM structures with reuseable Components in a functional way. **It is a drop-in replacement for [Deku],** which takes much inspiration from [React] and other functional-style view libraries.
8 |
9 | [](https://travis-ci.org/rstacruz/decca "See test builds")
10 |
11 | **[Documentation →](http://ricostacruz.com/decca)**
12 | **[Playground →](http://codepen.io/rstacruz/pen/LkaKNp?editors=0010#0)**
13 |
14 |
15 |
16 | ## Installation
17 |
18 | Decca is available via npm for Browserify and Webpack. (Don't use npm? Get the standalone build from [brcdn.org](https://www.brcdn.org/?module=decca).)
19 |
20 | ```
21 | npm install --save --save-exact decca
22 | ```
23 |
24 | ## Components
25 |
26 | Components are mere functions or objects (not [classes!](https://facebook.github.io/react/docs/top-level-api.html#react.createclass)) that at least implement a `render()` function. See [components](docs/components.md) documentation for more information.
27 |
28 | ```js
29 | /* @jsx element */
30 | import { dom, element } from 'decca'
31 |
32 | function Message ({ props }) {
33 | return Hello there, {props.name}
34 | }
35 |
36 | // Render the app tree
37 | const render = dom.createRenderer(document.body)
38 | render()
39 | ```
40 |
41 | > Try out Decca in **[codepen.io](http://codepen.io/rstacruz/pen/LkaKNp?editors=0010#0)**.
42 |
43 | ## Usage
44 |
45 | See the [API reference](docs/api.md) and [Deku]'s documentation. Also see a [comparison with Deku](docs/about-deku.md).
46 |
47 | ## Acknowledgements
48 |
49 | Decca takes blatant inspiration from [Deku] by the amazing [Anthony Short] and friends.
50 |
51 | [Deku]: https://dekujs.github.io/deku
52 | [virtual-dom]: https://www.npmjs.com/package/virtual-dom
53 | [lifecycle hooks]: docs/components.md
54 | [Anthony Short]: https://github.com/anthonyshort
55 | [React]: https://facebook.github.io/react/
56 |
57 | ## Thanks
58 |
59 | **decca** © 2015+, Rico Sta. Cruz. Released under the [MIT] License.
60 | Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]).
61 |
62 | > [ricostacruz.com](http://ricostacruz.com) ·
63 | > GitHub [@rstacruz](https://github.com/rstacruz) ·
64 | > Twitter [@rstacruz](https://twitter.com/rstacruz)
65 |
66 | [MIT]: http://mit-license.org/
67 | [contributors]: http://github.com/rstacruz/decca/contributors
68 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | ## [v2.3.0]
2 | > Apr 8, 2017
3 |
4 | - [#327] - Update compatibility with SystemJS and JSPM. ([@mikz])
5 |
6 | ## [v2.2.2]
7 | > Aug 29, 2016
8 |
9 | - [#195] - Allow pure components in string renderer. ([#196], [@borisirota])
10 |
11 | [v2.2.2]: https://github.com/rstacruz/decca/compare/v2.2.1...v2.2.2
12 |
13 | ## [v2.2.1]
14 | > Aug 12, 2016
15 |
16 | - Make the npm package slimmer by excluding docs; no functional changes.
17 |
18 | [v2.2.1]: https://github.com/rstacruz/decca/compare/v2.2.0...v2.2.1
19 |
20 | ## [v2.2.0]
21 | > Aug 12, 2016
22 |
23 | - Pure (function-only) components are now supported.
24 |
25 | [v2.2.0]: https://github.com/rstacruz/decca/compare/v2.1.0...v2.2.0
26 |
27 | ## [v2.1.0]
28 | > Jun 1, 2016
29 |
30 | - [#12] - Fix not being able to use camelCase properties (like `accessKey`). ([#130], [@mikz])
31 |
32 | [v2.1.0]: https://github.com/rstacruz/decca/compare/v2.0.1...v2.1.0
33 |
34 | ## [v2.0.1]
35 | > Jan 16, 2016
36 |
37 | - Fix string rendering inserting random commas.
38 |
39 | [v2.0.1]: https://github.com/rstacruz/decca/compare/v2.0.0...v2.0.1
40 |
41 | ## [v2.0.0]
42 | > Jan 10, 2016
43 |
44 | - Removes `state`, `setState` and `initialState` - making decca fully 1:1 analogous to Deku v2.0.0-rc11 API. For states, use [deku-stateful].
45 |
46 | [deku-stateful]: https://www.npmjs.com/package/deku-stateful
47 | [v2.0.0]: https://github.com/rstacruz/decca/compare/v1.2.1...v2.0.0
48 |
49 | ## [v1.2.1]
50 | > Jan 8, 2016
51 |
52 | - More optimizations.
53 |
54 | ## [v1.2.0]
55 | > Jan 8, 2016
56 |
57 | - Some minor optimizations to make rendering faster between passes.
58 |
59 | ## [v1.0.0]
60 | > Jan 1, 2016
61 |
62 | - Renamed from 'not-deku' to 'decca'. No functional changes since v0.1.0.
63 |
64 | ## [v0.1.0]
65 | > Jan 1, 2016
66 |
67 | - Many changes... but we're now Deku v2 compliant.
68 |
69 | ## [v0.0.1]
70 | > Dec 30, 2015
71 |
72 | - Initial release.
73 |
74 | [v0.0.1]: https://github.com/rstacruz/decca/tree/v0.0.1
75 | [v0.1.0]: https://github.com/rstacruz/decca/compare/v0.0.1...v0.1.0
76 | [v1.0.0]: https://github.com/rstacruz/decca/compare/v0.1.0...v1.0.0
77 | [v1.2.0]: https://github.com/rstacruz/decca/compare/v1.0.0...v1.2.0
78 | [v1.2.1]: https://github.com/rstacruz/decca/compare/v1.2.0...v1.2.1
79 | [#12]: https://github.com/rstacruz/decca/issues/12
80 | [#130]: https://github.com/rstacruz/decca/issues/130
81 | [@mikz]: https://github.com/mikz
82 | [#195]: https://github.com/rstacruz/decca/issues/195
83 | [#196]: https://github.com/rstacruz/decca/issues/196
84 | [@borisirota]: https://github.com/borisirota
85 |
86 |
87 | [#327]: https://github.com/rstacruz/decca/issues/327
88 |
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "decca",
3 | "description": "Render interfaces using pure functions and virtual DOM, kinda",
4 | "version": "2.3.0",
5 | "author": "Rico Sta. Cruz ",
6 | "babel": {
7 | "presets": [
8 | "es2015"
9 | ],
10 | "plugins": [
11 | "syntax-jsx",
12 | [
13 | "transform-react-jsx",
14 | {
15 | "pragma": "element"
16 | }
17 | ]
18 | ]
19 | },
20 | "bugs": {
21 | "url": "https://github.com/rstacruz/decca/issues"
22 | },
23 | "dependencies": {
24 | "object-assign": "4.1.0",
25 | "simpler-debounce": "1.0.0",
26 | "virtual-dom": "2.1.1"
27 | },
28 | "devDependencies": {
29 | "@rstacruz/jsdoc-render-md": "1.3.1",
30 | "babel-cli": "6.24.0",
31 | "babel-plugin-syntax-jsx": "6.18.0",
32 | "babel-plugin-transform-react-jsx": "6.23.0",
33 | "babel-preset-es2015": "6.24.0",
34 | "babel-register": "6.24.0",
35 | "babelify": "7.3.0",
36 | "budo": "8.3.0",
37 | "deku": "2.0.0-rc16",
38 | "docpress": "0.6.13",
39 | "es5-shim": "4.5.9",
40 | "eslint": "2.11.1",
41 | "eslint-config-standard": "5.3.1",
42 | "eslint-config-standard-jsx": "1.2.0",
43 | "eslint-config-standard-react": "3.0.0",
44 | "eslint-engine": "0.2.0",
45 | "eslint-plugin-promise": "1.3.1",
46 | "eslint-plugin-react": "5.1.1",
47 | "eslint-plugin-standard": "1.3.2",
48 | "git-update-ghpages": "1.3.0",
49 | "jsdoc-parse": "1.2.7",
50 | "jsdom": "9.3.0",
51 | "jsdom-global": "2.1.1",
52 | "tap-dev-tool": "1.3.0",
53 | "tap-diff": "0.1.1",
54 | "tape": "4.5.1",
55 | "tape-dom": "0.0.10",
56 | "tape-watch": "2.1.0",
57 | "watchify": "3.7.0"
58 | },
59 | "directories": {
60 | "test": "test"
61 | },
62 | "homepage": "https://github.com/rstacruz/decca#readme",
63 | "keywords": [
64 | "deku",
65 | "dom",
66 | "elm",
67 | "functional",
68 | "react",
69 | "redux",
70 | "virtual"
71 | ],
72 | "license": "MIT",
73 | "main": "lib/index.js",
74 | "jsnext:main": "src/index.js",
75 | "repository": {
76 | "type": "git",
77 | "url": "git+https://github.com/rstacruz/decca.git"
78 | },
79 | "scripts": {
80 | "build": "babel src --out-dir lib",
81 | "prepublish": "npm run update",
82 | "test": "tape -r babel-register test/index.js | tap-diff --color",
83 | "test:budo": "budo test/index.js --live --open -- -t babelify --debug",
84 | "test:watch": "npm run test -- --watch",
85 | "watch": "babel -w src --out-dir lib",
86 | "lint": "eslint-check",
87 | "update": "(cat support/api_header.md; jsdoc-parse -s none -f src/*.js | jsdoc-render-md) > docs/api.md"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/widget.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import getId from './id'
4 |
5 | const createElement = require('virtual-dom/create-element')
6 | const diff = require('virtual-dom/diff')
7 | const patch = require('virtual-dom/patch')
8 | const assign = require('object-assign')
9 |
10 | /*
11 | * A widget that represents a component.
12 | * We need to do this to hook lifecycle hooks properly.
13 | *
14 | * Consumed in virtual-dom like so:
15 | *
16 | * h('div', {}, [ new Widget(el, model, build) ])
17 | *
18 | * widget.init()
19 | * widget.update()
20 | * widget.remove()
21 | */
22 |
23 | export default function Widget ({ component, props, children }, model, build) {
24 | if (!props) props = {}
25 | this.component = component
26 | this.build = build
27 |
28 | // The parameters to be passed onto the component's functions.
29 | this.model = assign({}, { props, children }, model)
30 | }
31 |
32 | Widget.prototype.type = 'Widget'
33 |
34 | /*
35 | * On widget creation, do the virtual-dom createElement() dance
36 | */
37 |
38 | Widget.prototype.init = function () {
39 | const id = setId(this, getId())
40 |
41 | // Create the virtual-dom tree
42 | const el = this.component.render(this.model)
43 | this.el = el
44 | this.tree = this.build(el) // virtual-dom vnode
45 | this.rootNode = createElement(this.tree) // DOM element
46 | this.rootNode._dekuId = id // so future update() and destroy() can see it
47 |
48 | // Trigger
49 | trigger(this, 'onCreate')
50 |
51 | // Export
52 | return this.rootNode
53 | }
54 |
55 | /*
56 | * On update, diff with the previous (also a Widget)
57 | */
58 |
59 | Widget.prototype.update = function (previous, domNode) {
60 | setId(this, domNode._dekuId)
61 |
62 | // Re-render the component
63 | const el = this.component.render(this.model)
64 |
65 | // If it was memoized, don't patch.
66 | // Just make this widget a copy of the previous.
67 | if (previous.el === el) {
68 | this.tree = previous.tree
69 | this.rootNode = previous.rootNode
70 | this.el = el
71 | return
72 | }
73 |
74 | this.tree = this.build(el)
75 |
76 | // Patch the DOM node
77 | var delta = diff(previous.tree, this.tree)
78 | this.rootNode = patch(previous.rootNode, delta)
79 | this.el = el
80 |
81 | trigger(this, 'onUpdate')
82 | }
83 |
84 | /*
85 | * On destroy, trigger the onRemove hook.
86 | */
87 |
88 | Widget.prototype.destroy = function (domNode) {
89 | setId(this, domNode._dekuId)
90 | trigger(this, 'onRemove')
91 | }
92 |
93 | /*
94 | * Updates the model with things that it can have when `id` is available.
95 | * This is because `id`'s aren't always available when Widget is initialized,
96 | * so these can't be in the ctor.
97 | */
98 |
99 | function setId (widget, id) {
100 | widget.model.path = id
101 | return id
102 | }
103 |
104 | /*
105 | * Trigger a Component lifecycle event.
106 | */
107 |
108 | function trigger (widget, hook, id) {
109 | if (!widget.component[hook]) return
110 | return widget.component[hook](widget.model)
111 | }
112 |
--------------------------------------------------------------------------------
/docs/components.md:
--------------------------------------------------------------------------------
1 | # Components
2 |
3 | Components are functions that return JSX objects (not [classes!](https://facebook.github.io/react/docs/top-level-api.html#react.createclass)). Here's an example of a [pure component](#pure-component):
4 |
5 | ```js
6 | /* @jsx element */
7 | import { element } from 'decca'
8 |
9 | function Button ({props}) {
10 | return
11 | }
12 |
13 | module.exports = Button
14 | ```
15 |
16 | ## Lifecycle hooks
17 |
18 | Components can also be objects that implement a `render()` function. In this form, it can have additional lifecycle hooks.
19 |
20 | ```js
21 | function render ({props}) {
22 | return
23 | }
24 |
25 | function onCreate ({props}) {
26 | ...
27 | }
28 |
29 | module.exports = { render, onCreate }
30 | ```
31 |
32 | An object component can have these functions:
33 |
34 | | Function | Description
35 | |---|---
36 | | __render()__ | Called every [render()](api.md#render) pass.
37 | | __onCreate()__ | Called after first render() when the DOM is constructed. Use this for side-effects like DOM bindings.
38 | | __onUpdate()__ | Called after every render() except the first one.
39 | | __onRemove()__ | Called after the component is removed. Use this for side effects like cleaning up `document` DOM bindings.
40 |
41 |
42 |
43 | ## Model
44 |
45 | A model is an Object passed onto every function in a component. It has these properties:
46 |
47 | | Property | Description
48 | |---|---
49 | | __props__ | An Object with the properties passed to the component.
50 | | __children__ | An array of children in a component.
51 | | __context__ | The `context` object passed onto [render()](api.md#render)
52 | | __dispatch__ | The `dispatch` object passed onto [dom.createRenderer()](api.md#dom.createrenderer).
53 | | __path__ | A unique ID of the component instance.
54 |
55 |
56 |
57 | ## Nesting components
58 |
59 | Well, yes, of course you can.
60 |
61 | ```js
62 | /** @jsx element */
63 | import { dom, element } from 'decca'
64 |
65 | const App = {
66 | render () {
67 | return
68 |
69 |
70 | }
71 | }
72 |
73 | const Button = {
74 | render ({props}) {
75 | return
76 | }
77 | }
78 |
79 | // Render the app tree
80 | render = dom.createRenderer(document.body)
81 | render()
82 | ```
83 |
84 | ## Pure components
85 |
86 | You may define a component as a function. This is useful if you don't need any of the lifecycle hooks (`onCreate`, `onUpdate`, `onRemove`). It will act like a component's `render()` function. *(Version v2.2+)*
87 |
88 | ```js
89 | function Message ({props}) {
90 | return Hello, {props.name}
91 | }
92 |
93 | render = dom.createRenderer(document.body)
94 | render()
95 | ```
96 |
97 | ## JSX
98 |
99 | Decca supports JSX syntax. See [JSX](jsx.md) for details on how to set it up.
100 |
101 | ## Further references
102 |
103 | See Deku's documentation:
104 |
105 | - [Lifecycle hooks](https://dekujs.github.io/deku/docs/advanced/lifecycle.html)
106 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API reference
2 |
3 | ```js
4 | import { dom, element, string } from 'decca'
5 | ```
6 |
7 | ## decca/dom
8 |
9 |
10 |
11 | ### createRenderer()
12 |
13 |
14 | createRenderer(el, dispatch?)
15 |
16 | | Param | Type | Description |
17 | | --- | --- | --- |
18 | | `el` | DOMNode | The DOM element to mount to |
19 | | `dispatch` | function, _optional_ | The dispatch function to the store |
20 |
21 | > Returns render
22 |
23 |
24 | Creates a renderer function that will update the given `rootEl` DOM Node if
25 | called. Returns a renderer function; see [render](#render).
26 |
27 | ### render
28 |
29 |
30 | render(element, context?)
31 |
32 | | Param | Type | Description |
33 | | --- | --- | --- |
34 | | `element` | Element | Virtual element to render; given by [element()](#element) |
35 | | `context` | *, _optional_ | The context to be passed onto the components as `context` |
36 |
37 | > Returns void *(callback)*
38 |
39 |
40 | A renderer function returned by [createRenderer()](#createrenderer).
41 |
42 | ## decca/element
43 |
44 |
45 |
46 | ### element()
47 |
48 |
49 | element(tag, props, ...children?)
50 |
51 | | Param | Type | Description |
52 | | --- | --- | --- |
53 | | `tag` | string | Tag name (eg, `'div'`) |
54 | | `props` | object | Properties |
55 | | `children` | Element | string, _optional_ | Children |
56 |
57 | > Returns Element
58 |
59 |
60 | Returns a vnode (*Element*) to be consumed by [render()](#render).
61 | This is compatible with JSX. Returns An element.
62 |
63 | ### Element
64 |
65 |
66 | { tag, props, children }
67 |
68 | | Param | Type | Description |
69 | | --- | --- | --- |
70 | | `tag` | string | Tag name (eg, `'div'`) |
71 | | `props` | object | Properties |
72 | | `children` | (Element|string)[] | Children |
73 |
74 |
75 | A vnode (*Element*) to be consumed by [render()](#render).
76 | This is generated via [element()](#element).
77 |
78 | ## decca/string
79 |
80 |
81 |
82 | ### render()
83 |
84 |
85 | render(el, context?)
86 |
87 | | Param | Type | Description |
88 | | --- | --- | --- |
89 | | `el` | Element | The Element to render |
90 | | `context` | *, _optional_ | The context to be passed onto components |
91 |
92 | > Returns string
93 |
94 |
95 | Renders an element into a string without using the DOM. Returns the rendered HTML string.
96 |
--------------------------------------------------------------------------------
/test/deku/create_dom_renderer_test.js:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import { dom, element } from '../../src'
3 | const { createRenderer } = dom
4 |
5 | test('rendering elements', t => {
6 | let el = document.createElement('div')
7 | let render = createRenderer(el)
8 |
9 | render()
10 | t.equal(el.innerHTML, '', 'rendered')
11 |
12 | render()
13 | t.equal(el.innerHTML, '', 'attributed added')
14 |
15 | render(
)
16 | t.equal(el.innerHTML, '
', 'root replaced')
17 |
18 | render()
19 | t.equal(el.innerHTML, '', 'child replaced')
20 |
21 | render()
22 | t.equal(el.innerHTML, '', 'root removed')
23 |
24 | render(Hello
)
25 | t.equal(el.innerHTML, 'Hello
', 'root added')
26 |
27 | t.end()
28 | })
29 |
30 | test('moving elements using keys', t => {
31 | let el = document.createElement('div')
32 | let render = createRenderer(el)
33 |
34 | render(
35 |
36 |
37 |
38 |
39 |
40 | )
41 |
42 | let span = el.childNodes[0].childNodes[1]
43 |
44 | render(
45 |
46 |
47 |
48 |
49 |
50 | )
51 |
52 | t.equal(
53 | el.innerHTML,
54 | '
',
55 | 'elements rearranged'
56 | )
57 |
58 | t.equal(
59 | span,
60 | el.childNodes[0].childNodes[0],
61 | 'element is moved'
62 | )
63 |
64 | t.end()
65 | })
66 |
67 | test('emptying the container', t => {
68 | let el = document.createElement('div')
69 | el.innerHTML = ''
70 | let render = createRenderer(el)
71 | render()
72 | t.equal(
73 | el.innerHTML,
74 | '',
75 | 'container emptied'
76 | )
77 | t.end()
78 | })
79 |
80 | test('context should be passed down all elements', t => {
81 | let Form = {
82 | render ({ props, context }) {
83 | return
84 |
My form
85 |
86 |
87 |
88 |
89 | }
90 | }
91 | let Button = {
92 | render ({ props, context }) {
93 | t.equal(context.hello, 'there')
94 | return
95 | }
96 | }
97 | let el = document.createElement('div')
98 | let render = createRenderer(el)
99 | t.plan(1)
100 | render(, { hello: 'there' })
101 | t.end()
102 | })
103 |
104 | test('context should be passed down across re-renders', t => {
105 | let Form = {
106 | render () {
107 | return
108 | }
109 | }
110 | let Button = {
111 | render ({ props, context }) {
112 | t.equal(context, 'the context', 'context is passed down')
113 | return
114 | }
115 | }
116 | let el = document.createElement('div')
117 | let render = createRenderer(el)
118 | t.plan(2)
119 | render(, 'the context')
120 | render(, 'the context')
121 | t.end()
122 | })
123 |
124 | test('rendering numbers as text elements', t => {
125 | let el = document.createElement('div')
126 | let render = createRenderer(el)
127 | render({5})
128 | t.equal(
129 | el.innerHTML,
130 | '5',
131 | 'number rendered correctly'
132 | )
133 | t.end()
134 | })
135 |
136 | test('rendering the same node', t => {
137 | let el = document.createElement('div')
138 | let render = createRenderer(el)
139 | var node =
140 | render(node)
141 | render(node)
142 | t.equal(
143 | el.innerHTML,
144 | '',
145 | 'samenode is handled'
146 | )
147 | t.end()
148 | })
149 |
150 | test('context should be passed down across re-renders even after disappearance', t => {
151 | let Form = {
152 | render ({ props }) {
153 | return {props.visible ? : []}
154 | }
155 | }
156 | let Button = {
157 | render ({ props, context }) {
158 | t.equal(context, 'the context', 'context is passed down')
159 | return
160 | }
161 | }
162 | let el = document.createElement('div')
163 | let render = createRenderer(el)
164 | t.plan(2)
165 | render(, 'the context')
166 | render(, 'the context')
167 | render(, 'the context')
168 | t.end()
169 | })
170 |
--------------------------------------------------------------------------------
/test/basic_test.js:
--------------------------------------------------------------------------------
1 | import { element } from '../src'
2 | import test from 'tape'
3 | import r from './support/r'
4 |
5 | test('basic non-component', (t) => {
6 | const { div } = r(hello
)
7 | t.equal(div.innerHTML, 'hello
')
8 | t.end()
9 | })
10 |
11 | test('class name', (t) => {
12 | const { div } = r(hola
)
13 | t.equal(div.innerHTML, 'hola
')
14 | t.end()
15 | })
16 |
17 | test('attributes', (t) => {
18 | const { div } = r(hola
)
19 | t.equal(div.innerHTML, 'hola
')
20 | t.end()
21 | })
22 |
23 | test('interpolation', (t) => {
24 | const {div} = r(hey {'John'}
)
25 | t.equal(div.innerHTML, 'hey John
')
26 | t.end()
27 | })
28 |
29 | test('basic component', (t) => {
30 | const App = {
31 | render () { return hello
}
32 | }
33 |
34 | const { div } = r()
35 | t.equal(div.innerHTML, 'hello
')
36 | t.end()
37 | })
38 |
39 | test('props', (t) => {
40 | const Button = {
41 | render ({ props }) {
42 | return
43 | }
44 | }
45 |
46 | const App = {
47 | render () {
48 | return hi.
49 | }
50 | }
51 |
52 | const { div } = r()
53 | t.equal(div.innerHTML, 'hi.
')
54 | t.end()
55 | })
56 |
57 | test('context', (t) => {
58 | const Button = {
59 | render ({ props, context }) {
60 | t.equal(context, 'CTX')
61 | return
62 | }
63 | }
64 |
65 | const App = {
66 | render ({ context }) {
67 | t.equal(context, 'CTX')
68 | return hi.
69 | }
70 | }
71 |
72 | const { div } = r(, 'CTX')
73 | t.equal(div.innerHTML, 'hi.
')
74 | t.end()
75 | })
76 |
77 | test('events', (t) => {
78 | // not consistently working on jsdom. why?
79 | if (navigator.userAgent.indexOf('Node.js') === -1) {
80 | t.plan(1)
81 | }
82 |
83 | const App = {
84 | render ({ context }) {
85 | return
86 | }
87 | }
88 |
89 | function yo () {
90 | t.pass('clicked')
91 | }
92 |
93 | const { div } = r()
94 | document.body.appendChild(div)
95 |
96 | var event = document.createEvent('MouseEvent')
97 | event.initEvent('click', true, true)
98 | document.querySelector('#sup').dispatchEvent(event)
99 | t.end()
100 | })
101 |
102 | test('onUpdate', (t) => {
103 | t.plan(1)
104 | const App = {
105 | onUpdate ({ context, path }) {
106 | t.equal(context, 'CTX', 'context is available onUpdate')
107 | },
108 | render ({ context }) {
109 | return
110 | }
111 | }
112 |
113 | const { div, render } = r(, 'CTX')
114 | render(, 'CTX')
115 | t.end()
116 | })
117 |
118 | test('onRemove', (t) => {
119 | t.plan(3)
120 | const App = {
121 | onRemove ({ context }) {
122 | t.pass('onRemove was called')
123 | t.equal(context, 'CTX', 'context is available onRemove')
124 | },
125 | render ({ context }) { return }
126 | }
127 |
128 | const { div, render } = r(, 'CTX')
129 | render()
130 | t.equal(div.innerHTML, '', 'renders correctly')
131 | t.end()
132 | })
133 |
134 | test('onRemove skipping', (t) => {
135 | t.plan(0)
136 | const App = {
137 | onRemove ({ context }) { t.fail('not supposed to call onRemove') },
138 | render ({ context }) { return }
139 | }
140 |
141 | const { div } = r(, 'CTX')
142 | t.end()
143 | })
144 |
145 | test('onCreate', (t) => {
146 | t.plan(2)
147 | const App = {
148 | onCreate ({ context }) {
149 | t.equal(context, 'CTX', 'context available onCreate')
150 | },
151 | render ({ context }) {
152 | return
153 | }
154 | }
155 |
156 | const { div, render } = r(, 'CTX')
157 | render(, 'CTX')
158 | t.equal(div.innerHTML, '', 'renders correctly')
159 | t.end()
160 | })
161 |
162 | test('class in component', (t) => {
163 | t.plan(2)
164 |
165 | const App = {
166 | render ({ props }) {
167 | t.equal(props.class, 'app', 'has class')
168 | return hello
169 | }
170 | }
171 |
172 | const { div } = r()
173 | t.equal(div.innerHTML, 'hello
', 'renders')
174 | t.end()
175 | })
176 |
177 | test('pure components', (t) => {
178 | t.plan(2)
179 |
180 | function App ({ props }) {
181 | t.equal(props.class, 'app', 'has class')
182 | return hello
183 | }
184 |
185 | const { div } = r()
186 | t.equal(div.innerHTML, 'hello
', 'renders')
187 | t.end()
188 | })
189 |
190 |
--------------------------------------------------------------------------------
/test/string_test.js:
--------------------------------------------------------------------------------
1 | import { element, string } from '../src'
2 | import test from 'tape'
3 |
4 | test('string: basic non-component', (t) => {
5 | const output = string.render(hello
)
6 | t.equal(output, 'hello
', 'renders')
7 | t.end()
8 | })
9 |
10 | test('string: basic non-component via render()', (t) => {
11 | const output = string.render(hello
)
12 | t.equal(output, 'hello
', 'renders')
13 | t.end()
14 | })
15 |
16 | test('string: basic component', (t) => {
17 | const App = {
18 | render: () => hello
19 | }
20 | const output = string.render()
21 | t.equal(output, 'hello
', 'renders')
22 | t.end()
23 | })
24 |
25 | test('string: basic nested component', (t) => {
26 | const Button = {
27 | render: () => hello
28 | }
29 | const App = {
30 | render: () =>
31 | }
32 | const output = string.render()
33 | t.equal(output, 'hello
', 'renders')
34 | t.end()
35 | })
36 |
37 | test('string: contexts in nested components', (t) => {
38 | t.plan(3)
39 | const Button = {
40 | render: ({ context }) => {
41 | t.equal(context, 'my context', 'context in level 2')
42 | return hello
43 | }
44 | }
45 | const App = {
46 | render: ({ context }) => {
47 | t.equal(context, 'my context', 'context in level 1')
48 | return
49 | }
50 | }
51 | const output = string.render(, 'my context')
52 | t.equal(output, '', 'renders')
53 | t.end()
54 | })
55 |
56 | test('string: paths in nested components', (t) => {
57 | t.plan(3)
58 | const Button = {
59 | render: ({ path }) => {
60 | t.ok(path, 'has path')
61 | return hello
62 | }
63 | }
64 | const App = {
65 | render: ({ path }) => {
66 | t.ok(path, 'has path')
67 | return
68 | }
69 | }
70 | const output = string.render()
71 | t.equal(output, 'hello
', 'renders')
72 | t.end()
73 | })
74 |
75 | test('string: components with children', (t) => {
76 | const Button = {
77 | render: ({ props }) => {props.children}
78 | }
79 | const App = {
80 | render: () =>
81 | }
82 | const output = string.render()
83 | t.equal(output, 'hello
', 'renders')
84 | t.end()
85 | })
86 |
87 | test('string: basic pure component', (t) => {
88 | const App = () => hello
89 | const output = string.render()
90 | t.equal(output, 'hello
', 'renders')
91 | t.end()
92 | })
93 |
94 | test('string: basic nested pure component', (t) => {
95 | const Button = () => hello
96 | const App = () =>
97 | const output = string.render()
98 | t.equal(output, 'hello
', 'renders')
99 | t.end()
100 | })
101 |
102 | test('string: basic nested pure component inside standard', (t) => {
103 | const Button = () => hello
104 | const App = {
105 | render: () =>
106 | }
107 | const output = string.render()
108 | t.equal(output, 'hello
', 'renders')
109 | t.end()
110 | })
111 |
112 | test('string: basic nested standard component inside pure', (t) => {
113 | const Button = {
114 | render: () => hello
115 | }
116 | const App = () =>
117 | const output = string.render()
118 | t.equal(output, 'hello
', 'renders')
119 | t.end()
120 | })
121 |
122 | test('string: contexts in nested pure components', (t) => {
123 | t.plan(3)
124 | const Button = ({ context }) => {
125 | t.equal(context, 'my context', 'context in level 2')
126 | return hello
127 | }
128 | const App = ({ context }) => {
129 | t.equal(context, 'my context', 'context in level 1')
130 | return
131 | }
132 | const output = string.render(, 'my context')
133 | t.equal(output, '', 'renders')
134 | t.end()
135 | })
136 |
137 | test('string: paths in nested pure components', (t) => {
138 | t.plan(3)
139 | const Button = ({ path }) => {
140 | t.ok(path, 'has path')
141 | return hello
142 | }
143 | const App = ({ path }) => {
144 | t.ok(path, 'has path')
145 | return
146 | }
147 | const output = string.render()
148 | t.equal(output, 'hello
', 'renders')
149 | t.end()
150 | })
151 |
152 | test('string: pure components with children', (t) => {
153 | const Button = ({ props }) => {props.children}
154 | const App = () =>
155 | const output = string.render()
156 | t.equal(output, 'hello
', 'renders')
157 | t.end()
158 | })
159 |
--------------------------------------------------------------------------------