Comparing version 0.4.12 to 0.5.0
0.5.0 / 2015-08-03 | ||
================== | ||
* Merge pull request #217 from dekujs/0.5.0pre | ||
* Added missing dep | ||
* Updated Readme | ||
* Removing dom | ||
* Fixing test for IE10 | ||
* Reducing the concurrent tests. Seems to break Sauce. | ||
* Fixed broken test | ||
* Removed old test file | ||
* Merge branch 'master' into 0.5.0pre | ||
* Merge branch 'Yomguithereal-shadow-dom-events' | ||
* Merge branch 'shadow-dom-events' of https://github.com/Yomguithereal/deku into Yomguithereal-shadow-dom-events | ||
* Merge branch 'master' of ssh://github.com/dekujs/deku into 0.5.0pre | ||
* Updated virtual-element to 1.1.0 | ||
* Revert "Remove Sources" | ||
* Adding a heuristic to determine the best HTMLElement on which to attach events listener. The intention here is to enable deku to render into document fragments such as Shadow DOM. | ||
* Test cleanup | ||
* Fixed bug with default props | ||
* Components can be functions instead of objects | ||
* Updated repo in package.json | ||
* Cleaned up some junk | ||
* Derp | ||
* Updated zuul location | ||
* Update dependencies | ||
* Updated zuul to work with tape | ||
* Fixed bug with replacing text nodes with undefined | ||
* Refactored the tests | ||
* Decoupled string renderer from DOM renderer dep | ||
* Added snazzy for better linting errors | ||
* Using object-defaults | ||
* Cleaned up the Makefile | ||
* Fixed lint issues | ||
* Removed sources | ||
* Removed DOM pooling | ||
* Using dependency for svg checking | ||
* Switch to use virtual-element | ||
0.4.12 / 2015-07-28 | ||
@@ -3,0 +42,0 @@ ================== |
@@ -21,10 +21,2 @@ /** | ||
exports.renderString = require('./stringify') | ||
/** | ||
* Create virtual elements. | ||
*/ | ||
exports.element = | ||
exports.createElement = | ||
exports.dom = require('./virtual') | ||
exports.renderString = require('./stringify') |
@@ -6,23 +6,14 @@ /** | ||
var raf = require('component-raf') | ||
var Pool = require('dom-pool') | ||
var walk = require('dom-walk') | ||
var isDom = require('is-dom') | ||
var uid = require('get-uid') | ||
var keypath = require('object-path') | ||
var utils = require('./utils') | ||
var events = require('./events') | ||
var svg = require('./svg') | ||
var events = require('./events') | ||
var defaults = utils.defaults | ||
var defaults = require('object-defaults') | ||
var forEach = require('fast.js/forEach') | ||
var assign = require('fast.js/object/assign') | ||
var reduce = require('fast.js/reduce') | ||
var isPromise = require('is-promise') | ||
var nodeType = require('./node-type') | ||
/** | ||
* These elements won't be pooled | ||
*/ | ||
var avoidPooling = ['input', 'textarea', 'select', 'option']; | ||
/** | ||
* Expose `dom`. | ||
@@ -52,3 +43,2 @@ */ | ||
var entities = {} | ||
var pools = {} | ||
var handlers = {} | ||
@@ -70,3 +60,2 @@ var mountQueue = [] | ||
var options = defaults(assign({}, app.options || {}, opts || {}), { | ||
pooling: true, | ||
batching: true | ||
@@ -78,3 +67,3 @@ }) | ||
*/ | ||
var rootElement = getRootElement(container) | ||
addNativeEventListeners() | ||
@@ -210,4 +199,5 @@ | ||
var component = entity.component | ||
if (!component.render) throw new Error('Component needs a render function') | ||
var result = component.render(entity.context, setState(entity)) | ||
var fn = typeof component === 'function' ? component : component.render | ||
if (!fn) throw new Error('Component needs a render function') | ||
var result = fn(entity.context, setState(entity)) | ||
if (!result) throw new Error('Render function must return an element.') | ||
@@ -229,3 +219,3 @@ return result | ||
return function (nextState) { | ||
updateEntityStateAsync(entity, nextState) | ||
updateEntityState(entity, nextState) | ||
} | ||
@@ -284,3 +274,3 @@ } | ||
} | ||
removeAllChildren(container); | ||
removeAllChildren(container) | ||
container.appendChild(currentNativeElement) | ||
@@ -300,2 +290,3 @@ } else if (currentElement !== app.element) { | ||
isRendering = false | ||
} | ||
@@ -309,4 +300,4 @@ | ||
function flushMountQueue () { | ||
var entityId | ||
while (entityId = mountQueue.pop()) { | ||
while (mountQueue.length > 0) { | ||
var entityId = mountQueue.shift() | ||
var entity = entities[entityId] | ||
@@ -424,4 +415,5 @@ trigger('afterRender', entity, [entity.context, entity.nativeElement]) | ||
function toNative (entityId, path, vnode) { | ||
switch (vnode.type) { | ||
switch (nodeType(vnode)) { | ||
case 'text': return toNativeText(vnode) | ||
case 'empty': return toNativeEmptyElement(entityId, path) | ||
case 'element': return toNativeElement(entityId, path, vnode) | ||
@@ -438,4 +430,4 @@ case 'component': return toNativeComponent(entityId, path, vnode) | ||
function toNativeText (vnode) { | ||
return document.createTextNode(vnode.data) | ||
function toNativeText (text) { | ||
return document.createTextNode(text) | ||
} | ||
@@ -448,18 +440,12 @@ | ||
function toNativeElement (entityId, path, vnode) { | ||
var el | ||
var attributes = vnode.attributes | ||
var children = vnode.children | ||
var tagName = vnode.tagName | ||
var el | ||
var tagName = vnode.type | ||
var childNodes = vnode.children | ||
// create element either from pool or fresh. | ||
if (!options.pooling || !canPool(tagName)) { | ||
if (svg.isElement(tagName)) { | ||
el = document.createElementNS(svg.namespace, tagName) | ||
} else { | ||
el = document.createElement(tagName) | ||
} | ||
if (svg.isElement(tagName)) { | ||
el = document.createElementNS(svg.namespace, tagName) | ||
} else { | ||
var pool = getPool(tagName) | ||
el = cleanup(pool.pop()) | ||
if (el.parentNode) el.parentNode.removeChild(el) | ||
el = document.createElement(tagName) | ||
} | ||
@@ -472,8 +458,4 @@ | ||
// store keys on the native element for fast event handling. | ||
el.__entity__ = entityId | ||
el.__path__ = path | ||
// add children. | ||
forEach(children, function (child, i) { | ||
forEach(childNodes, function (child, i) { | ||
var childEl = toNative(entityId, path + '.' + i, child) | ||
@@ -483,2 +465,6 @@ if (!childEl.parentNode) el.appendChild(childEl) | ||
// store keys on the native element for fast event handling. | ||
el.__entity__ = entityId | ||
el.__path__ = path | ||
return el | ||
@@ -488,2 +474,13 @@ } | ||
/** | ||
* Create a native element from a virtual element. | ||
*/ | ||
function toNativeEmptyElement (entityId, path) { | ||
var el = document.createElement('noscript') | ||
el.__entity__ = entityId | ||
el.__path__ = path | ||
return el | ||
} | ||
/** | ||
* Create a native element from a component. | ||
@@ -493,3 +490,3 @@ */ | ||
function toNativeComponent (entityId, path, vnode) { | ||
var child = new Entity(vnode.component, vnode.props, entityId) | ||
var child = new Entity(vnode.type, assign({ children: vnode.children }, vnode.attributes), entityId) | ||
children[entityId][path] = child.id | ||
@@ -512,9 +509,13 @@ return mountEntity(child) | ||
function diffNode (path, entityId, prev, next, el) { | ||
var leftType = nodeType(prev) | ||
var rightType = nodeType(next) | ||
// Type changed. This could be from element->text, text->ComponentA, | ||
// ComponentA->ComponentB etc. But NOT div->span. These are the same type | ||
// (ElementNode) but different tag name. | ||
if (prev.type !== next.type) return replaceElement(entityId, path, el, next) | ||
if (leftType !== rightType) return replaceElement(entityId, path, el, next) | ||
switch (next.type) { | ||
switch (rightType) { | ||
case 'text': return diffText(prev, next, el) | ||
case 'empty': return el | ||
case 'element': return diffElement(path, entityId, prev, next, el) | ||
@@ -530,3 +531,3 @@ case 'component': return diffComponent(path, entityId, prev, next, el) | ||
function diffText (previous, current, el) { | ||
if (current.data !== previous.data) el.data = current.data | ||
if (current !== previous) el.data = current | ||
return el | ||
@@ -547,5 +548,8 @@ } | ||
function keyMapReducer (acc, child) { | ||
if (child.key != null) { | ||
acc[child.key] = child | ||
function keyMapReducer (acc, child, i) { | ||
if (child && child.attributes && child.attributes.key != null) { | ||
acc[child.attributes.key] = { | ||
element: child, | ||
index: i | ||
} | ||
hasKeys = true | ||
@@ -585,4 +589,4 @@ } | ||
entityId, | ||
leftNode, | ||
rightNode, | ||
leftNode.element, | ||
rightNode.element, | ||
childNodes[leftNode.index] | ||
@@ -620,3 +624,3 @@ ) | ||
rightPath, | ||
rightNode | ||
rightNode.element | ||
) | ||
@@ -635,29 +639,29 @@ } | ||
// Removals | ||
if (rightNode == null) { | ||
if (rightNode === undefined) { | ||
removeElement( | ||
entityId, | ||
path + '.' + leftNode.index, | ||
childNodes[leftNode.index] | ||
path + '.' + i, | ||
childNodes[i] | ||
) | ||
continue | ||
} | ||
// New Node | ||
if (leftNode == null) { | ||
positions[rightNode.index] = toNative( | ||
if (leftNode === undefined) { | ||
positions[i] = toNative( | ||
entityId, | ||
path + '.' + rightNode.index, | ||
path + '.' + i, | ||
rightNode | ||
) | ||
continue | ||
} | ||
// Updated | ||
if (leftNode && rightNode) { | ||
positions[leftNode.index] = diffNode( | ||
path + '.' + leftNode.index, | ||
entityId, | ||
leftNode, | ||
rightNode, | ||
childNodes[leftNode.index] | ||
) | ||
} | ||
positions[i] = diffNode( | ||
path + '.' + i, | ||
entityId, | ||
leftNode, | ||
rightNode, | ||
childNodes[i] | ||
) | ||
} | ||
@@ -669,3 +673,3 @@ } | ||
var target = el.childNodes[newPosition] | ||
if (childEl !== target) { | ||
if (childEl && childEl !== target) { | ||
if (target) { | ||
@@ -710,3 +714,3 @@ el.insertBefore(childEl, target) | ||
function diffComponent (path, entityId, prev, next, el) { | ||
if (next.component !== prev.component) { | ||
if (next.type !== prev.type) { | ||
return replaceElement(entityId, path, el, next) | ||
@@ -718,3 +722,3 @@ } else { | ||
if (targetId) { | ||
updateEntityProps(targetId, next.props) | ||
updateEntityProps(targetId, assign({ children: next.children }, next.attributes)) | ||
} | ||
@@ -731,3 +735,3 @@ | ||
function diffElement (path, entityId, prev, next, el) { | ||
if (next.tagName !== prev.tagName) return replaceElement(entityId, path, el, next) | ||
if (next.type !== prev.type) return replaceElement(entityId, path, el, next) | ||
diffAttributes(prev, next, el, entityId, path) | ||
@@ -794,11 +798,2 @@ diffChildren(path, entityId, prev, next, el) | ||
el.parentNode.removeChild(el) | ||
// Return all of the elements in this node tree to the pool | ||
// so that the elements can be re-used. | ||
if (options.pooling) { | ||
walk(el, function (node) { | ||
if (!isElement(node) || !canPool(node.tagName)) return | ||
getPool(node.tagName.toLowerCase()).push(node) | ||
}) | ||
} | ||
} | ||
@@ -874,2 +869,6 @@ | ||
function setAttribute (entityId, path, el, name, value) { | ||
if (!value) { | ||
removeAttribute(entityId, path, el, name) | ||
return | ||
} | ||
if (events[name]) { | ||
@@ -919,3 +918,3 @@ addEvent(entityId, path, events[name], value) | ||
case 'value': | ||
el[name] = "" | ||
el[name] = '' | ||
break | ||
@@ -958,47 +957,2 @@ default: | ||
/** | ||
* Get the pool for a tagName, creating it if it | ||
* doesn't exist. | ||
* | ||
* @param {String} tagName | ||
* | ||
* @return {Pool} | ||
*/ | ||
function getPool (tagName) { | ||
var pool = pools[tagName] | ||
if (!pool) { | ||
var poolOpts = svg.isElement(tagName) ? | ||
{ namespace: svg.namespace, tagName: tagName } : | ||
{ tagName: tagName } | ||
pool = pools[tagName] = new Pool(poolOpts) | ||
} | ||
return pool | ||
} | ||
/** | ||
* Clean up previously used native element for reuse. | ||
* | ||
* @param {HTMLElement} el | ||
*/ | ||
function cleanup (el) { | ||
removeAllChildren(el) | ||
removeAllAttributes(el) | ||
return el | ||
} | ||
/** | ||
* Remove all the attributes from a node | ||
* | ||
* @param {HTMLElement} el | ||
*/ | ||
function removeAllAttributes (el) { | ||
for (var i = el.attributes.length - 1; i >= 0; i--) { | ||
var name = el.attributes[i].name | ||
el.removeAttribute(name) | ||
} | ||
} | ||
/** | ||
* Remove all the child nodes from an element | ||
@@ -1040,26 +994,6 @@ * | ||
args.push(update) | ||
var result = trigger(name, entity, args) | ||
if (result) { | ||
updateEntityStateAsync(entity, result) | ||
} | ||
trigger(name, entity, args) | ||
} | ||
/** | ||
* Update the entity state using a promise | ||
* | ||
* @param {Entity} entity | ||
* @param {Promise} promise | ||
*/ | ||
function updateEntityStateAsync (entity, value) { | ||
if (isPromise(value)) { | ||
value.then(function (newState) { | ||
updateEntityState(entity, newState) | ||
}) | ||
} else { | ||
updateEntityState(entity, value) | ||
} | ||
} | ||
/** | ||
* Update an entity to match the latest rendered vode. We always | ||
@@ -1078,3 +1012,3 @@ * replace the props on the component when composing them. This | ||
var entity = entities[entityId] | ||
entity.pendingProps = nextProps | ||
entity.pendingProps = defaults({}, nextProps, entity.component.defaultProps || {}) | ||
entity.dirty = true | ||
@@ -1230,3 +1164,3 @@ invalidate() | ||
forEach(events, function (eventType) { | ||
document.addEventListener(eventType, handleEvent, true) | ||
rootElement.addEventListener(eventType, handleEvent, true) | ||
}) | ||
@@ -1241,3 +1175,3 @@ } | ||
forEach(events, function (eventType) { | ||
document.removeEventListener(eventType, handleEvent, true) | ||
rootElement.removeEventListener(eventType, handleEvent, true) | ||
}) | ||
@@ -1262,3 +1196,3 @@ } | ||
event.delegateTarget = target | ||
if (false === fn(event)) break | ||
if (fn(event) === false) break | ||
} | ||
@@ -1281,10 +1215,5 @@ target = target.parentNode | ||
if (entity) { | ||
var update = setState(entity) | ||
var result = fn.call(null, e, entity.context, update) | ||
if (result) { | ||
updateEntityStateAsync(entity, result) | ||
} | ||
return result | ||
fn.call(null, e, entity.context, setState(entity)) | ||
} else { | ||
return fn.call(null, e) | ||
fn.call(null, e) | ||
} | ||
@@ -1327,3 +1256,2 @@ }) | ||
entities: entities, | ||
pools: pools, | ||
handlers: handlers, | ||
@@ -1366,3 +1294,3 @@ connections: connections, | ||
this.context = {} | ||
this.context.id = this.id; | ||
this.context.id = this.id | ||
this.context.props = defaults(props || {}, component.defaultProps || {}) | ||
@@ -1379,23 +1307,22 @@ this.context.state = this.component.initialState ? this.component.initialState(this.context.props) : {} | ||
/** | ||
* Should we pool an element? | ||
*/ | ||
function canPool(tagName) { | ||
return avoidPooling.indexOf(tagName) < 0 | ||
} | ||
/** | ||
* Get a nested node using a path | ||
* Retrieve the nearest 'body' ancestor of the given element or else the root | ||
* element of the document in which stands the given element. | ||
* | ||
* @param {HTMLElement} el The root node '0' | ||
* @param {String} path The path string eg. '0.2.43' | ||
* This is necessary if you want to attach the events handler to the correct | ||
* element and be able to dispatch events in document fragments such as | ||
* Shadow DOM. | ||
* | ||
* @param {HTMLElement} el The element on which we will render an app. | ||
* @return {HTMLElement} The root element on which we will attach the events | ||
* handler. | ||
*/ | ||
function getNodeAtPath(el, path) { | ||
var parts = path.split('.') | ||
parts.shift() | ||
while (parts.length) { | ||
el = el.childNodes[parts.pop()] | ||
function getRootElement (el) { | ||
while (el.parentElement) { | ||
if (el.tagName === 'BODY' || !el.parentElement) { | ||
return el | ||
} | ||
el = el.parentElement | ||
} | ||
return el | ||
} |
@@ -1,4 +0,4 @@ | ||
var utils = require('./utils') | ||
var events = require('./events') | ||
var defaults = utils.defaults | ||
var defaults = require('object-defaults') | ||
var nodeType = require('./node-type') | ||
var type = require('component-type') | ||
@@ -50,8 +50,9 @@ /** | ||
function stringifyNode (node, path) { | ||
switch (node.type) { | ||
case 'text': return node.data | ||
switch (nodeType(node)) { | ||
case 'empty': return '<noscript />' | ||
case 'text': return node | ||
case 'element': | ||
var children = node.children | ||
var attributes = node.attributes | ||
var tagName = node.tagName | ||
var tagName = node.type | ||
var innerHTML = attributes.innerHTML | ||
@@ -70,3 +71,3 @@ var str = '<' + tagName + attrs(attributes) + '>' | ||
return str | ||
case 'component': return stringify(node.component, node.props) | ||
case 'component': return stringify(node.type, node.attributes) | ||
} | ||
@@ -91,5 +92,5 @@ | ||
for (var key in attributes) { | ||
var value = attributes[key] | ||
if (key === 'innerHTML') continue | ||
if (events[key]) continue | ||
str += attr(key, attributes[key]) | ||
if (isValidAttributeValue(value)) str += attr(key, attributes[key]) | ||
} | ||
@@ -111,1 +112,14 @@ return str | ||
} | ||
/** | ||
* Is a value able to be set a an attribute value? | ||
* | ||
* @param {Any} value | ||
* | ||
* @return {Boolean} | ||
*/ | ||
function isValidAttributeValue (value) { | ||
var valueType = type(value) | ||
return (valueType === 'string' || valueType === 'boolean' || valueType === 'number') | ||
} |
107
lib/svg.js
@@ -1,104 +0,5 @@ | ||
/** | ||
* This file lists the supported SVG elements used by the | ||
* renderer. We may add better SVG support in the future | ||
* that doesn't require whitelisting elements. | ||
*/ | ||
exports.namespace = 'http://www.w3.org/2000/svg' | ||
/** | ||
* Supported SVG elements | ||
* | ||
* @type {Array} | ||
*/ | ||
exports.elements = { | ||
'animate': true, | ||
'circle': true, | ||
'defs': true, | ||
'ellipse': true, | ||
'g': true, | ||
'line': true, | ||
'linearGradient': true, | ||
'mask': true, | ||
'path': true, | ||
'pattern': true, | ||
'polygon': true, | ||
'polyline': true, | ||
'radialGradient': true, | ||
'rect': true, | ||
'stop': true, | ||
'svg': true, | ||
'text': true, | ||
'tspan': true | ||
module.exports = { | ||
isElement: require('is-svg-element').isElement, | ||
isAttribute: require('is-svg-attribute'), | ||
namespace: 'http://www.w3.org/2000/svg' | ||
} | ||
/** | ||
* Supported SVG attributes | ||
*/ | ||
exports.attributes = { | ||
'cx': true, | ||
'cy': true, | ||
'd': true, | ||
'dx': true, | ||
'dy': true, | ||
'fill': true, | ||
'fillOpacity': true, | ||
'fontFamily': true, | ||
'fontSize': true, | ||
'fx': true, | ||
'fy': true, | ||
'gradientTransform': true, | ||
'gradientUnits': true, | ||
'markerEnd': true, | ||
'markerMid': true, | ||
'markerStart': true, | ||
'offset': true, | ||
'opacity': true, | ||
'patternContentUnits': true, | ||
'patternUnits': true, | ||
'points': true, | ||
'preserveAspectRatio': true, | ||
'r': true, | ||
'rx': true, | ||
'ry': true, | ||
'spreadMethod': true, | ||
'stopColor': true, | ||
'stopOpacity': true, | ||
'stroke': true, | ||
'strokeDasharray': true, | ||
'strokeLinecap': true, | ||
'strokeOpacity': true, | ||
'strokeWidth': true, | ||
'textAnchor': true, | ||
'transform': true, | ||
'version': true, | ||
'viewBox': true, | ||
'x1': true, | ||
'x2': true, | ||
'x': true, | ||
'y1': true, | ||
'y2': true, | ||
'y': true, | ||
} | ||
/** | ||
* Is element's namespace SVG? | ||
* | ||
* @param {String} name | ||
*/ | ||
exports.isElement = function (name) { | ||
return name in exports.elements | ||
} | ||
/** | ||
* Are element's attributes SVG? | ||
* | ||
* @param {String} attr | ||
*/ | ||
exports.isAttribute = function (attr) { | ||
return attr in exports.attributes | ||
} |
{ | ||
"name": "deku", | ||
"version": "0.4.12", | ||
"repository": "segmentio/deku", | ||
"version": "0.5.0", | ||
"license": "MIT", | ||
"repository": "dekujs/deku", | ||
"description": "Create view components using a virtual DOM", | ||
@@ -9,32 +10,28 @@ "main": "lib/index.js", | ||
"babelify": "^6.1.1", | ||
"benchmark": "^1.0.0", | ||
"bfc": "~0.3.1", | ||
"browserify": "^8.1.1", | ||
"bump": "git://github.com/ianstormtaylor/bump.git#0.4.2", | ||
"component-classes": "^1.2.3", | ||
"duo-test": "^0.3.13", | ||
"browserify": "^11.0.0", | ||
"bump": "*", | ||
"envify": "^3.4.0", | ||
"es6-promise": "^2.1.1", | ||
"memoizee": "^0.3.8", | ||
"hihat": "^2.4.0", | ||
"minify": "^1.4.11", | ||
"mochify": "^2.1.1", | ||
"phantomjs": "^1.9.17", | ||
"standard": "^3.6.1", | ||
"snazzy": "^1.0.1", | ||
"standard": "^4.5.4", | ||
"tap-dev-tool": "^1.3.0", | ||
"tape": "^4.0.1", | ||
"trigger-event": "^1.0.0", | ||
"zuul": "~1.16.4" | ||
"virtual-element": "^1.1.0", | ||
"zuul": "^3.2.0" | ||
}, | ||
"dependencies": { | ||
"array-flatten": "^1.0.2", | ||
"component-emitter": "^1.1.3", | ||
"component-raf": "^1.2.0", | ||
"component-type": "^1.1.0", | ||
"dom-pool": "^0.1.1", | ||
"dom-walk": "^0.1.1", | ||
"fast.js": "^0.1.1", | ||
"get-uid": "^1.0.1", | ||
"is-dom": "^1.0.5", | ||
"is-promise": "^2.0.0", | ||
"object-path": "^0.8.1", | ||
"sliced": "0.0.5" | ||
"is-svg-attribute": "^1.0.2", | ||
"is-svg-element": "^1.0.1", | ||
"object-defaults": "^0.1.0", | ||
"object-path": "^0.9.2" | ||
} | ||
} | ||
} |
363
README.md
@@ -5,41 +5,21 @@ # Deku | ||
A library for creating UI components using virtual DOM as an alternative to [React](https://github.com/facebook/react). Deku has a smaller footprint (~8kb), a functional API, and doesn't support legacy browsers. | ||
A library for creating UI components using virtual DOM as an alternative to [React](https://github.com/facebook/react). Deku has a smaller footprint (~6kb), a functional API, and doesn't support legacy browsers. | ||
To install: | ||
``` | ||
npm install deku | ||
npm install deku virtual-element | ||
``` | ||
> You can also use Duo, Bower or [download the files manually](https://github.com/dekujs/deku/releases). | ||
You can also use Duo, Bower or [download the files manually](https://github.com/dekujs/deku/releases). | ||
[Components](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) are just plain objects that have a render function instead of using classes or constructors: | ||
## Example | ||
```js | ||
// button.js | ||
import element from 'virtual-element' | ||
import {render,tree} from 'deku' | ||
import MyButton from './button' | ||
let Button = { | ||
render (component) { | ||
let {props, state} = component | ||
return <button class="Button" type={props.kind}>{props.children}</button> | ||
}, | ||
afterUpdate (component, prevProps, prevState, updateState) { | ||
let {props, state} = component | ||
if (!state.clicked) { | ||
updateState({ clicked: true }) | ||
} | ||
} | ||
} | ||
export {Button} | ||
``` | ||
Components are then rendered by mounting it in a tree: | ||
```js | ||
import {Button} from './button' | ||
import {tree,render,renderString} from 'deku' | ||
let app = tree( | ||
<Button kind="submit">Hello World!</Button> | ||
var app = tree( | ||
<div class="MyApp"> | ||
<MyButton>Hello World!</MyButton> | ||
</div> | ||
) | ||
@@ -50,287 +30,180 @@ | ||
Trees can be rendered on the server too: | ||
## Introduction | ||
```js | ||
let str = renderString(app) | ||
``` | ||
Deku is a DOM renderer for virtual elements that also allows us to define custom element types. It runs diffing algorithm on these virtual elements to update the real DOM in a performant way. | ||
## Docs | ||
**Heads up:** These examples are written using ES2015 syntax. You'll want to make sure you're familiar with modules and destructuring to follow along. | ||
* [Installing](https://github.com/dekujs/deku/blob/master/docs/guides/install.md) | ||
* [Component Spec](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) | ||
* [Using JSX](https://github.com/dekujs/deku/blob/master/docs/guides/jsx.md) | ||
* [Client + Server Rendering Example](https://github.com/dekujs/todomvc) | ||
* [Community resources](https://github.com/stevenmiller888/awesome-deku) | ||
Virtual elements are plain objects that represent real DOM elements: | ||
## Components | ||
Each element of your UI can be broken into encapsulated components. These components manage the state for the UI element and tell it how to render. In Deku components are just plain objects: | ||
```js | ||
function render (component) { | ||
let {props, state} = component | ||
return <button class="Button">{props.children}</button> | ||
{ | ||
type: 'button', | ||
attributes: { class: 'Button' }, | ||
children: props.children | ||
} | ||
export default {render} | ||
``` | ||
There is no concept of classes or use of `this`. We can import this component using the standard module syntax: | ||
Which can then be rendered by Deku to the DOM. This example will render a button to the `document.body` with a class of `Button`: | ||
```js | ||
import Button from './button' | ||
``` | ||
import {render,tree} from 'deku' | ||
[Read more about components](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) | ||
var button = { | ||
type: 'button', | ||
attributes: { class: 'Button' } | ||
} | ||
## Rendering Components | ||
// Create an app | ||
var app = tree(button) | ||
To render this to the DOM we need to create a `tree`. This is one of the other main differences between React and Deku. The `tree` will manage loading data, communicating between components and allows us to use plugins on the entire application. | ||
```js | ||
import {element,tree} from 'deku' | ||
var app = tree(<Button>Hello World</Button>) | ||
// Automatically re-renders the app when state changes | ||
render(app, document.body) | ||
``` | ||
The `app` object has only a couple of methods: | ||
You can define your own custom elements that can contain their own state. These are called **components**. Components are objects that (at the very least) have a render function. This render function is passed in a component object with these properties: | ||
* `.set(name, value)` to set environment data | ||
* `.option(name, value)` to set rendering options | ||
* `.mount(vnode)` to change the virtual element currently mounted | ||
* `.use(fn)` to use a plugin. The function is called with the `app` object. | ||
* `props`: This is any external data | ||
* `state`: This is any internal data that is hidden from the world | ||
* `id`: The instance id of the component | ||
You can render this tree anyway you like, you just need a renderer for it. Let's use the DOM renderer for the client: | ||
Here's an example `App` component that renders a paragraph: | ||
```js | ||
import Button from './button' | ||
import {element,tree,render} from 'deku' | ||
import {render,app} from 'deku' | ||
var app = tree(<Button>Hello World</Button>) | ||
render(app, document.body) | ||
``` | ||
// Define our custom element. The render method should | ||
// return a new virtual element. | ||
var App = { | ||
render: function ({ props, state }) { | ||
return { | ||
type: 'p', | ||
attributes: { color: props.color } | ||
} | ||
} | ||
} | ||
And render the same thing to a string on the server: | ||
// Then create a virtual element with our custom type | ||
var app = tree({ | ||
type: App, // <- custom type instead of a string | ||
attributes: { color: 'red' } // <- these become 'props' | ||
}) | ||
```js | ||
import koa from 'koa' | ||
import {element,tree,renderString} from 'deku' | ||
let app = koa() | ||
app.use(function *() { | ||
this.body = renderString(tree(<Button>Hello World</Button>)) | ||
}) | ||
// And render it to the DOM | ||
render(app, document.body) | ||
``` | ||
And you can isolate functionality by using plugins. These plugins can call `set` to add data to the tree that your components can then access through their props: | ||
## Virtual Elements | ||
```js | ||
app.use(analytics) | ||
app.use(router) | ||
app.use(api(writeKey)) | ||
``` | ||
But these virtual elements aren't very easy to read. The good news is that you can use other libraries to add a DSL for creating these objects: | ||
## Composition | ||
* [virtual-element](https://github.com/dekujs/virtual-element) | ||
* [magic-virtual-element](https://github.com/dekujs/magic-virtual-element) | ||
You can compose components easily by just requiring them and using them in the render function: | ||
So you can use the `virtual-element` module to easily create these objects instead: | ||
```js | ||
import Button from './button' | ||
import Sheet from './sheet' | ||
function render (component) { | ||
return ( | ||
<div class="MyCoolApp"> | ||
<Sheet> | ||
<Button style="danger">One</Button> | ||
<Button style="happy">Two</Button> | ||
</Sheet> | ||
</div> | ||
) | ||
} | ||
element('div', { class: "App" }, [ | ||
element('button', { class: "Button" }, 'Click Me!') | ||
]) | ||
``` | ||
## Event handlers | ||
And if you're using `virtual-element` [you can also use JSX](https://github.com/dekujs/deku/blob/master/docs/guides/jsx.md) to make rendering nodes more developer friendly. This is equivalant to the previous example: | ||
Deku doesn't use any form of synthetic events because we can just capture every event in newer browsers. There are special attributes you can add to virtual elements that act as hooks to add event listeners: | ||
```js | ||
function render (component) { | ||
let {props, state} = component | ||
return <button onClick={clicked}>{props.children}</button> | ||
} | ||
function clicked (event, component, updateState) { | ||
alert('You clicked it') | ||
} | ||
```jsx | ||
<div class="App"> | ||
<button class="Button">Click Me!</button> | ||
</div> | ||
``` | ||
You can [view all event handlers](https://github.com/dekujs/deku/blob/master/lib/events.js) in code. | ||
JSX might seem offensive at first, but if you're already using Babel you get JSX for free. Think of it as a more familiar way to define tree structures. **The rest of the examples will assume we're using JSX.** You can go ahead and imagine the same syntax using the `virtual-element` DSL or the raw object format. | ||
You can access the event, the component and update the state in event handlers: | ||
So the previous app example would look like this using JSX (notice that we're importing the `virtual-element` module this time): | ||
```js | ||
function clicked (event, component, updateState) { | ||
let {props,state} = component | ||
} | ||
``` | ||
import element from 'virtual-element' | ||
import {render,tree} from 'deku' | ||
To access the element you'll usually want to `event.target`. This is the element the event was triggered on. We also set `event.delegateTarget` that will always be set to the element that owns the handler if it was a deeper element that triggered the event. | ||
## Lifecycle hooks | ||
Just like the `render` function, component lifecycle hooks are just plain functions: | ||
```js | ||
function afterUpdate (component, prevProps, prevState, updateState) { | ||
let {props, state} = component | ||
if (!state.clicked) { | ||
updateState({ clicked: true }) | ||
// Define our custom element | ||
var App = { | ||
render: function ({ props }) { | ||
return <p color={props.color}>Hello World</p> | ||
} | ||
} | ||
``` | ||
We have hooks for `beforeMount`, `afterMount`, `beforeUpdate`, `afterUpdate`, `beforeUnmount` and two new hooks - `beforeRender` and `afterRender` that are called on every pass, unlike the update hooks. We've found that these extra hooks have allowed us to write cleaner code and worry less about the state of the component. | ||
var app = tree(<App color="red" />) | ||
[Learn more about the lifecycle hooks](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) | ||
## Prop Validation | ||
Prop validation isn't handle by Deku, but because we're dealing with pure functions it's easy enough to just compose it yourself using [some other validation library](https://www.npmjs.com/package/validate): | ||
```js | ||
import schema from 'validate' | ||
let {assert} = schema({ | ||
name: { | ||
type: 'string', | ||
required: true, | ||
message: 'name is required' | ||
} | ||
}) | ||
let render = function({ props, state }) { | ||
assert(props) | ||
return <div></div> | ||
} | ||
// And render it to the DOM | ||
render(app, document.body) | ||
``` | ||
## External data and communication | ||
## Custom Elements | ||
It's often useful for components to have access to data from the outside world without needing to pass it down through components. You can set data on your `tree` and components can ask for it using `propTypes`. | ||
So now we can start defining components in their own module and export them. Let's create a custom button element: | ||
First we set some data on the app: | ||
```js | ||
app.set('currentUser', { | ||
id: 12435, | ||
username: 'anthonyshort', | ||
name: 'Anthony Short' | ||
}) | ||
``` | ||
// button.js | ||
import {element} from 'virtual-element' | ||
Then in our components we define the prop using the `source` option: | ||
```js | ||
let propTypes = { | ||
user: { | ||
source: 'currentUser' | ||
} | ||
let MyButton = { | ||
render ({props}) { | ||
return <button class="Button">{props.children}</button> | ||
} | ||
} | ||
``` | ||
Whenever we change that value in our app all components that depend on it will be re-rendered with the latest value. We use this pattern to pass functions down to interact with the API: | ||
```js | ||
app.set('updateProject', function (project, updates) { | ||
api.projects.update(project, updates) | ||
}) | ||
export {MyButton} | ||
``` | ||
Which the component can access using `props.updateProject`. Although it may not be as complex or optimized as Relay and GraphQL it's extremely simple and covers most use cases we've run into so far. We even use this pattern to treat the router as a data source: | ||
Then we can import it and render it in the same way: | ||
```js | ||
router.on('/projects/:id', function (params) { | ||
let project = api.projects.get(params.id) | ||
app.set('currentRoute', { | ||
name: 'view project', | ||
project: project | ||
}) | ||
}) | ||
``` | ||
// app.js | ||
import element from 'virtual-element' | ||
import {MyButton} from './button' | ||
import {render,tree} from 'deku' | ||
This means we don't need to use some complex routing library. We just treat it like all other types of external data and components will render as needed. | ||
// We're using our custom MyButton element | ||
var app = tree( | ||
<div class="MyApp"> | ||
<MyButton>Hello World!</MyButton> | ||
</div> | ||
) | ||
## Keys | ||
Sometimes when you're rendering a list of items you want them to be moved instead of trashed during the diff. Deku supports this using the `key` attribute on components: | ||
```js | ||
function render (component) { | ||
let {items} = component.props | ||
let projects = items.map(function (project) { | ||
return <ProjectItem key={project.id} project={project} /> | ||
}) | ||
return <div class="ProjectsList">{projects}</div> | ||
} | ||
render(app, document.body) | ||
``` | ||
At the moment we only support the `key` attribute on components for simplicity. Things become slightly more hairy when moving elements around within components. So far we haven't ran into a case where this has been a major problem. | ||
You can also render these same elements and custom elements on the server using `renderString` instead of `render`: | ||
## Experimental: ES7 async functions | ||
The purpose of most lifecycle hooks is usually to update the state, either by inspecting the DOM or fetching some external resources. We can simplify the concept of the lifecycle hooks by making the pure using ES7 async functions. | ||
```js | ||
async function afterMount ({ props }, el) { | ||
var items = await request(props.url) | ||
var projects = await Projects.getAll() | ||
// server.js | ||
import element from 'virtual-element' | ||
import {MyButton} from './button' | ||
import {renderString,tree} from 'deku' | ||
// Return an object to update state | ||
return { | ||
items: items, | ||
projects: projects, | ||
loaded: true | ||
} | ||
} | ||
let html = renderString(tree( | ||
<div class="MyApp"> | ||
<MyButton>Hello World!</MyButton> | ||
</div> | ||
)) | ||
``` | ||
Instead of using the `updateState` function we can just return an object that will be merged in with the current state. We can do this because the lifecycle hooks are able to return a promise that resolves into a state change. All you need to do is return a promise and resolve it with an object. | ||
That's all there is to it. Components can also have [hook functions](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) so you can do some work when they are created, removed or updated, and you can [add state](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) to your components. | ||
We could do this with standard promises too: | ||
## Next steps | ||
```js | ||
function afterMount ({ props }, el) { | ||
return request(props.url) | ||
.then(Projects.getAll) | ||
.then(function(items, projects){ | ||
return { | ||
items: items, | ||
projects: projects, | ||
loaded: true | ||
} | ||
}) | ||
} | ||
``` | ||
* [Installing](https://github.com/dekujs/deku/blob/master/docs/guides/install.md) | ||
* [Component API](https://github.com/dekujs/deku/blob/master/docs/guides/components.md) | ||
* [Using JSX](https://github.com/dekujs/deku/blob/master/docs/guides/jsx.md) | ||
* [Client + Server Rendering Example](https://github.com/dekujs/todomvc) | ||
* [Community resources](https://github.com/stevenmiller888/awesome-deku) | ||
* [Contributing to Deku](https://github.com/dekujs/deku/blob/master/docs/guides/development.md) | ||
## innerHTML | ||
## Tests | ||
You can set a string of html to be set as `innerHTML` using the `innerHTML` attribute on your virtual elements: | ||
Deku is built with Browserify. You can run the tests in a browser by running `make test`. Learn how to build and work on Deku [in the documentation](https://github.com/dekujs/deku/blob/master/docs/guides/development.md). | ||
``` | ||
<div innerHTML="<span>hi</span>" /> | ||
``` | ||
**Deku doesn't do any sanitizing of the HTML string so you'll want to do that yourself to prevent XSS attacks.** | ||
## Tests | ||
[![Sauce Test Status](https://saucelabs.com/browser-matrix/deku.svg)](https://saucelabs.com/u/deku) | ||
## Developing | ||
Deku is built with Browserify. You can run the tests in a browser by running `make test`. | ||
## License | ||
MIT. See [LICENSE.md](http://github.com/dekujs/deku/blob/master/LICENSE.md) | ||
The MIT License (MIT) Copyright (c) 2015 Anthony Short |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
1683891
10
14
21729
1
15
208
1
+ Addedis-svg-attribute@^1.0.2
+ Addedis-svg-element@^1.0.1
+ Addedobject-defaults@^0.1.0
+ Addedis-svg-attribute@1.2.0(transitive)
+ Addedis-svg-element@1.0.1(transitive)
+ Addedobject-defaults@0.1.0(transitive)
+ Addedobject-path@0.9.2(transitive)
- Removedarray-flatten@^1.0.2
- Removeddom-pool@^0.1.1
- Removeddom-walk@^0.1.1
- Removedis-promise@^2.0.0
- Removedsliced@0.0.5
- Removedarray-flatten@1.1.1(transitive)
- Removeddom-pool@0.1.1(transitive)
- Removeddom-walk@0.1.2(transitive)
- Removedis-promise@2.2.2(transitive)
- Removedobject-path@0.8.1(transitive)
- Removedsliced@0.0.5(transitive)
Updatedobject-path@^0.9.2