├── .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 = '' 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 | [![Status](https://travis-ci.org/rstacruz/decca.svg?branch=master)](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 |
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: () =>
49 | } 50 | } 51 | const output = string.render(, 'my context') 52 | t.equal(output, '
hello
', '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 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 = () =>
131 | } 132 | const output = string.render(, 'my context') 133 | t.equal(output, '
hello
', '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 155 | const output = string.render() 156 | t.equal(output, '
hello
', 'renders') 157 | t.end() 158 | }) 159 | --------------------------------------------------------------------------------