diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore index b950123..84bf96f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist lib es npm-debug.log +.idea/ diff --git a/docs/api/create-app.md b/docs/api/create-app.md index de449cb..5b2cb19 100644 --- a/docs/api/create-app.md +++ b/docs/api/create-app.md @@ -5,7 +5,10 @@ Returns a `render` function that you can use to render elements within `DOMEleme ### Arguments 1. `el` _(HTMLElement)_: A container element that will have virtual elements rendered inside of it. The element will never be touched. -2. `dispatch` _(Function)_: A function that can receive actions from the interface. This function will be passed into every component. It usually takes an [action](http://redux.js.org/docs/basics/Actions.html) that can be handled by a [store](http://redux.js.org/docs/basics/Store.html) +2. `dispatch` _(Function)_ [optional]: A function that can receive actions from the interface. This function will be passed into every component. It usually takes an [action](http://redux.js.org/docs/basics/Actions.html) that can be handled by a [store](http://redux.js.org/docs/basics/Store.html) +3. `options` _(Object)_ [optional]: A plain object that contains renderer options: + - `reuseMarkup: true` will cause deku to reuse container contents when possible. This may be useful for isomorphic apps. + - `enableNodeRecycling: true` will enable pooling/recycling of existing DOM nodes. This should lead to reduced GC and memory usage. ### Returns @@ -34,7 +37,9 @@ render() ### Notes -The container DOM element should: -* **Not be the document.body**. You'll probably run into problems with other libraries. They'll often add elements to the `document.body` which can confuse the diff algorithm. -* **Be empty**. All elements inside of the container will be removed when a virtual element is rendered into it. The renderer needs to have complete control of all of the elements within the container. +* You should **avoid using document.body as the container element**. You'll probably run into problems with other libraries. They'll often add elements to the `document.body` which can confuse the diff algorithm. + +* When the container element is not empty, **deku may try to reuse container contents**. Read [this page](/deku/docs/tips/pre-rendered.md) to learn more about working with pre-rendered elements. + +. All elements inside of the container will be removed when a virtual element is rendered into it, unless markup reuse option is on. The renderer needs to have complete control of all of the elements within the container. diff --git a/docs/tips/pre-rendered.md b/docs/tips/pre-rendered.md new file mode 100644 index 0000000..9d26f25 --- /dev/null +++ b/docs/tips/pre-rendered.md @@ -0,0 +1,29 @@ +# Pre-rendered HTML Elements + +When the browser requests the HTML file, some of the elements may have been pre-rendered on the server-side. This can be done using deku's [`string.render`](/deku/docs/api/string.html). + + +```html +

pre-rendered text

+``` + +On the client side, if we just create a render function as usual, the first call to the render function would not do anything. This is because deku would assume that the container's pre-rendered content is properly rendered. + +```js +var render = createApp(document.getElementById("container"), { reuseMarkup: true }) +render(

pre-rendered text

) // do nothing, just assign event listeners, if any +``` + +If the virtualDOM describes a different HTML element, deku will rerender it completely, even if `reuseMarkup` flag is set. + +```js +var render = createApp(document.getElementById("container"), { reuseMarkup: true }) +render(

Meow!

) // will perform full rerender +``` + +### Notes + +- To avoid injecting 'react-id'-like attributes into tags, Deku hardly relies on order of pre-rendered nodes. +- If starting part of pre-rendered nodes matches virtualDOM, Deku will reuse this part, but will rerender the rest. +- So this leads to the tip: if some components of your app are to be rendered on client-side only, it would be wise to place their markup closer to the end of the container to avoid full rerender of all other components. + diff --git a/examples/basic/index.js b/examples/basic/index.js index 93b16e6..3b4ce20 100644 --- a/examples/basic/index.js +++ b/examples/basic/index.js @@ -1,12 +1,15 @@ import {createApp} from '../../src' import {view, update, init} from './app' +var div = document.createElement("div"); +document.body.appendChild(div); + /** * Create a DOM renderer for vnodes. It accepts a dispatch function as * a second parameter to handle all actions from the UI. */ -let render = createApp(document.body) +let render = createApp(div) /** * Update the UI with the latest state. diff --git a/package.json b/package.json index cf6d88f..94cb756 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "@f/set-attribute": "1.0.1", "@f/to-array": "1.1.1", "dift": "0.1.12", - "index-of": "0.2.0", "setify": "1.0.3", "union-type": "0.1.6" }, diff --git a/src/app/index.js b/src/app/index.js index 33544f3..2b6b199 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -1,12 +1,17 @@ import * as dom from '../dom' import {diffNode} from '../diff' -import empty from '@f/empty-element' import noop from '@f/noop' +import empty from '@f/empty-element' /** * Create a DOM renderer using a container element. Everything will be rendered - * inside of that container. Returns a function that accepts new state that can - * replace what is currently rendered. + * inside of that container. Can reuse markup inside the container (if any). + * Returns a function that accepts new state that can replace what is currently rendered. + * + * Options: + * - reuseMarkup (bool): try to reuse already rendered markup inside the container. + * This might be useful for isomorphic apps. + * - enableNodeRecycling (bool): try to reuse existing DOM nodes to minimize DOM GC */ export function createApp (container, handler = noop, options = {}) { @@ -15,9 +20,11 @@ export function createApp (container, handler = noop, options = {}) { let rootId = options.id || '0' let dispatch = effect => effect && handler(effect) - if (container) { - empty(container) + if (typeof handler !== 'function' && typeof handler === 'object') { + options = handler + handler = noop } + dom.enableNodeRecycling(options.enableNodeRecycling || false) let update = (newVnode, context) => { let changes = diffNode(oldVnode, newVnode, rootId) @@ -28,7 +35,18 @@ export function createApp (container, handler = noop, options = {}) { let create = (vnode, context) => { node = dom.createElement(vnode, rootId, dispatch, context) - if (container) container.appendChild(node) + if (container) { + empty(container) + container.appendChild(node) + } + oldVnode = vnode + return node + } + + let createWithReuse = (vnode, context) => { + let {DOMnode, attachEvents} = dom.createElementThenEvents(vnode, rootId, dispatch, context, container.firstChild) + node = DOMnode + attachEvents(container.firstChild) oldVnode = vnode return node } @@ -36,6 +54,10 @@ export function createApp (container, handler = noop, options = {}) { return (vnode, context = {}) => { return node !== null ? update(vnode, context) - : create(vnode, context) + : ( + !container || container.childNodes.length === 0 || !options.reuseMarkup + ? create(vnode, context) + : createWithReuse(vnode, context) + ) } } diff --git a/src/dom/create.js b/src/dom/create.js index 9c1ef92..3f403bc 100644 --- a/src/dom/create.js +++ b/src/dom/create.js @@ -1,48 +1,108 @@ -import createNativeElement from '@f/create-element' -import {createPath} from '../element' +import {createPath} from '../element/index' import {setAttribute} from './setAttribute' import isUndefined from '@f/is-undefined' import isString from '@f/is-string' import isNumber from '@f/is-number' import isNull from '@f/is-null' -const cache = {} +import Pool from './pool' + +const cache = new Pool() + +// node manipulation action types +const CREATE = 'create' +const REPLACE = 'replace' +const NOOP = 'noop' + +export default function createElement (vnode, path, dispatch, context) { + let DOM = createWithSideEffects(vnode, path, dispatch, context) + runSideEffects(DOM.sideEffects) + return DOM.element +} + +export function createElementThenEvents (vnode, path, dispatch, context, container = null) { + let DOM = createWithSideEffects(vnode, path, dispatch, context, {originNode: container}) + runSideEffects(DOM.sideEffects, {noEventListeners: true}) + return { + DOMnode: DOM.element, + attachEvents (PreRenderedElement) { + runSideEffects(DOM.sideEffects, {onlyEventListeners: true}, PreRenderedElement) + } + } +} + +export function enableNodeRecycling (flag) { + cache.enableRecycling(flag) +} + +export function storeInCache (node) { + cache.store(node) +} /** - * Create a real DOM element from a virtual element, recursively looping down. +/* Recursively traverse a tree of side effects created by `createWithSideEffects`, and run each side effect. + * Passing a third parameter DOMElement and it will traverse the DOMElement as well. + * + * A tree of side effects either takes in the value `null` or is a JSON object in the form + * {ofParent : P, ofChildren : C} + * where P is either an empty array or an array of functions + * and C is either an empty array or an array of trees of side effects + */ +function runSideEffects (sideEffects, option, DOMElement) { + if (sideEffects) { + sideEffects.ofParent.map((sideEffect) => { sideEffect(option, DOMElement) }) + sideEffects.ofChildren.map((child, index) => { + if (DOMElement) { + runSideEffects(child, option, DOMElement.childNodes[index]) + } else { + runSideEffects(child, option) + } + }) + } +} + +/** + * Create a DOM element with a tree of side effects from a virtual element by recursion. * When it finds custom elements it will render them, cache them, and keep going, * so they are treated like any other native element. */ - -export function createElement (vnode, path, dispatch, context) { +export function createWithSideEffects (vnode, path, dispatch, context, options = {}) { switch (vnode.type) { case 'text': - return createTextNode(vnode.nodeValue) + return createTextNode(vnode, options) case 'empty': - return getCachedElement('noscript') + return {element: cache.get('noscript'), sideEffects: null, action: CREATE} case 'thunk': - return createThunk(vnode, path, dispatch, context) + return createThunk(vnode, path, dispatch, context, options) case 'native': - return createHTMLElement(vnode, path, dispatch, context) + return createHTMLElement(vnode, path, dispatch, context, options) } } -function getCachedElement (type) { - let cached = cache[type] - if (isUndefined(cached)) { - cached = cache[type] = createNativeElement(type) +function createTextNode ({nodeValue}, {originNode}) { + let value = isString(nodeValue) || isNumber(nodeValue) + ? nodeValue + : '' + + // Determine action to perform after creating a text node + let action = CREATE + if (originNode && originNode.nodeValue) { + action = NOOP + if (originNode.nodeValue !== value) { + action = REPLACE + } } - return cached.cloneNode(false) -} -function createTextNode (text) { - let value = isString(text) || isNumber(text) - ? text - : '' - return document.createTextNode(value) + return { + element: action !== NOOP ? document.createTextNode(value) : null, + sideEffects: null, + action + } } -function createThunk (vnode, path, dispatch, context) { +function createThunk (vnode, path, dispatch, context, options) { let { props, children } = vnode + let effects = {ofParent: [], ofChildren: []} + let { onCreate } = vnode.options let model = { children, @@ -53,29 +113,86 @@ function createThunk (vnode, path, dispatch, context) { } let output = vnode.fn(model) let childPath = createPath(path, output.key || '0') - let DOMElement = createElement(output, childPath, dispatch, context) + let { element, sideEffects, action } = createWithSideEffects(output, childPath, dispatch, context, options) + effects.ofChildren.push(sideEffects) + if (onCreate) dispatch(onCreate(model)) vnode.state = { vnode: output, - model: model + model } - return DOMElement + return {element, sideEffects: effects, action} } -function createHTMLElement (vnode, path, dispatch, context) { - let { tagName, attributes, children } = vnode - let DOMElement = getCachedElement(tagName) +function createHTMLElement (vnode, path, dispatch, context, {originNode}) { + let DOMElement + let sideEffects = {ofParent: [], ofChildren: []} + let action = NOOP + + if (!originNode) { // no such element in markup -> create & append + DOMElement = cache.get(vnode.tagName) + action = CREATE + } else if (!originNode.tagName || originNode.tagName.toLowerCase() !== vnode.tagName.toLowerCase()) { + // found element, but with wrong type -> recreate & replace + let newDOMElement = cache.get(vnode.tagName) + Array.prototype.forEach.call(originNode.childNodes, (nodeChild) => { + newDOMElement.appendChild(nodeChild) + }) - for (let name in attributes) { - setAttribute(DOMElement, name, attributes[name]) + DOMElement = newDOMElement + action = REPLACE + } else { // found node and type matches -> reuse + DOMElement = originNode } - children.forEach((node, index) => { - if (isNull(node) || isUndefined(node)) return - let childPath = createPath(path, node.key || index) - let child = createElement(node, childPath, dispatch, context) - DOMElement.appendChild(child) + // traverse and [re]create child nodes if needed + let updateChild = nodesUpdater(DOMElement, path, dispatch, context) + vnode.children.forEach((node, index) => updateChild(node, index, sideEffects)) + + let attributes = Object.keys(vnode.attributes) + sideEffects.ofParent = attributes.map((name) => { + return function (option, element = DOMElement) { + setAttribute(element, name, vnode.attributes[name], null, option) + } }) - return DOMElement + return {element: DOMElement, sideEffects, action} +} + +/** + * Nodes updater factory: creates vnode for every found real DOM node. + * + * Hardly relies on order of child nodes, so it may cause big overhead even + * on minor differences, especially when difference occurs close to the + * beginning of the document. + * + * @returns {Function} + */ +function nodesUpdater (parentNode, path, dispatch, context) { + let i = 0 + + return (node, index, sideEffects) => { + if (isNull(node) || isUndefined(node)) return + + const originNode = parentNode.childNodes[i++] || null + const DOM = createWithSideEffects( + node, + createPath(path, node.key || index), + dispatch, + context, + {originNode} + ) + + sideEffects.ofChildren.push(DOM.sideEffects) + + switch (DOM.action) { + case CREATE: + parentNode.appendChild(DOM.element) + break + case REPLACE: + cache.store(parentNode.replaceChild(DOM.element, originNode)) + break + default: + } + } } diff --git a/src/dom/index.js b/src/dom/index.js index fd92b81..fb0d9ba 100644 --- a/src/dom/index.js +++ b/src/dom/index.js @@ -1,7 +1,10 @@ -import {createElement} from './create' -import {updateElement} from './update' +import createElement from './create' +import {createElementThenEvents, enableNodeRecycling} from './create' +import updateElement from './update' export { createElement, - updateElement + createElementThenEvents, + updateElement, + enableNodeRecycling } diff --git a/src/dom/pool.js b/src/dom/pool.js new file mode 100644 index 0000000..9624631 --- /dev/null +++ b/src/dom/pool.js @@ -0,0 +1,79 @@ +import createNativeElement from '@f/create-element' + +export default class Pool { + constructor () { + this.storage = {} + this._recyclingEnabled = false + } + + store (el) { + if (!this._recyclingEnabled || el._collected || !el.nodeType || el.nodeType !== 1 /* Node.ELEMENT_NODE == 1 */) { + return + } + el._collected = true + + if (el && el.parentNode) { + el.parentNode.removeChild(el) + } + + let tagName = el.tagName.toLowerCase() + if (!this.storage[tagName]) { + this.storage[tagName] = [] + } + + // little cleanup + el.className = '' + for (let i = 0; i < el.attributes.length; i++) { + el.removeAttribute(el.attributes[i].name) + } + + if (el.childNodes.length > 0) { + // Iterate backwards, because childNodes is live collection + for (let i = el.childNodes.length - 1; i >= 0; i--) { + this.store(el.childNodes[i]) + } + } + + this.storage[tagName].push(el) + } + + enableRecycling (flag) { + this._recyclingEnabled = flag + } + + get (tagName) { + tagName = tagName.toLowerCase() + if (this._recyclingEnabled && this.storage[tagName] && this.storage[tagName].length > 0) { + let el = this.storage[tagName].pop() + delete el._collected + return el + } + return createNativeElement(tagName) + } + + preallocate (tagName, size) { + if (!this._recyclingEnabled) { + return + } + + tagName = tagName.toLowerCase() + if (this.storage[tagName] && this.storage[tagName].length >= size) { + return + } + + if (!this.storage[tagName]) { + this.storage[tagName] = [] + } + + let difference = size - this.storage[tagName].length + for (let i = 0; i < difference; i++) { + this.store(createNativeElement(tagName), false) + } + } + + // for tests only + _getStorageSizeFor (tagName) { + return (this.storage[tagName] || []).length + } +} + diff --git a/src/dom/setAttribute.js b/src/dom/setAttribute.js index dfd38a4..1376327 100644 --- a/src/dom/setAttribute.js +++ b/src/dom/setAttribute.js @@ -1,7 +1,6 @@ import setNativeAttribute from '@f/set-attribute' import isValidAttribute from '@f/is-valid-attr' import isFunction from '@f/is-function' -import indexOf from 'index-of' import setValue from 'setify' import events from './events' @@ -28,16 +27,17 @@ export function removeAttribute (DOMElement, name, previousValue) { } } -export function setAttribute (DOMElement, name, value, previousValue) { - let eventType = events[name] - if (value === previousValue) { - return - } - if (eventType) { - if (isFunction(previousValue)) { - DOMElement.removeEventListener(eventType, previousValue) +export function setAttribute (DOMElement, name, value, previousValue, option = {}) { + if (value === previousValue) return + if (!option.noEventListeners) { + let eventType = events[name] + if (eventType) { + if (isFunction(previousValue)) DOMElement.removeEventListener(eventType, previousValue) + DOMElement.addEventListener(eventType, value) + return } - DOMElement.addEventListener(eventType, value) + } + if (option.onlyEventListeners) { return } if (!isValidAttribute(value)) { @@ -56,7 +56,9 @@ export function setAttribute (DOMElement, name, value, previousValue) { // Fix for IE/Safari where select is not correctly selected on change if (DOMElement.tagName === 'OPTION' && DOMElement.parentNode) { let select = DOMElement.parentNode - select.selectedIndex = indexOf(select.options, DOMElement) + if (select.options.indexOf) { + select.selectedIndex = select.options.indexOf(DOMElement) + } } break case 'value': diff --git a/src/dom/update.js b/src/dom/update.js index f3912e9..53beaf2 100644 --- a/src/dom/update.js +++ b/src/dom/update.js @@ -2,7 +2,7 @@ import {setAttribute, removeAttribute} from './setAttribute' import {isThunk, createPath} from '../element' import {Actions, diffNode} from '../diff' import reduceArray from '@f/reduce-array' -import {createElement} from './create' +import createElement, {storeInCache} from './create' import toArray from '@f/to-array' import forEach from '@f/foreach' import noop from '@f/noop' @@ -11,7 +11,7 @@ import noop from '@f/noop' * Modify a DOM element given an array of actions. */ -export function updateElement (dispatch, context) { +export default function updateElement (dispatch, context) { return (DOMElement, action) => { Actions.case({ sameNode: noop, @@ -33,13 +33,13 @@ export function updateElement (dispatch, context) { replaceNode: (prev, next, path) => { let newEl = createElement(next, path, dispatch, context) let parentEl = DOMElement.parentNode - if (parentEl) parentEl.replaceChild(newEl, DOMElement) + if (parentEl) storeInCache(parentEl.replaceChild(newEl, DOMElement)) DOMElement = newEl removeThunks(prev, dispatch) }, removeNode: (prev) => { removeThunks(prev) - DOMElement.parentNode.removeChild(DOMElement) + storeInCache(DOMElement.parentNode.removeChild(DOMElement)) DOMElement = null } }, action) @@ -62,7 +62,7 @@ function updateChildren (DOMElement, changes, dispatch, context) { insertAtIndex(DOMElement, index, createElement(vnode, path, dispatch, context)) }, removeChild: (index) => { - DOMElement.removeChild(childNodes[index]) + storeInCache(DOMElement.removeChild(childNodes[index])) }, updateChild: (index, actions) => { let _update = updateElement(dispatch, context) @@ -93,7 +93,7 @@ function updateThunk (DOMElement, prev, next, path, dispatch, context) { if (onUpdate) dispatch(onUpdate(model)) next.state = { vnode: nextNode, - model: model + model } return DOMElement } @@ -119,7 +119,7 @@ function removeThunks (vnode, dispatch) { */ export let insertAtIndex = (parent, index, el) => { - var target = parent.childNodes[index] + let target = parent.childNodes[index] if (target) { parent.insertBefore(el, target) } else { diff --git a/src/string/renderString.js b/src/string/renderString.js index 684ea89..03165ab 100644 --- a/src/string/renderString.js +++ b/src/string/renderString.js @@ -22,16 +22,16 @@ function attributesToString (attributes) { * object that will be given to all components. */ -export function renderString (vnode, context, path = '0') { +export function renderString (vnode, context, path = '0', opts = {}) { switch (vnode.type) { case 'text': return renderTextNode(vnode) case 'empty': return renderEmptyNode() case 'thunk': - return renderThunk(vnode, path, context) + return renderThunk(vnode, path, context, opts) case 'native': - return renderHTML(vnode, path, context) + return renderHTML(vnode, path, context, opts) } } @@ -43,13 +43,13 @@ function renderEmptyNode () { return '' } -function renderThunk (vnode, path, context) { +function renderThunk (vnode, path, context, opts) { let { props, children } = vnode let output = vnode.fn({ children, props, path, context }) - return renderString(output, context, path) + return renderString(output, context, path, opts) } -function renderHTML (vnode, path, context) { +function renderHTML (vnode, path, context, opts) { let {attributes, tagName, children} = vnode let innerHTML = attributes.innerHTML let str = '<' + tagName + attributesToString(attributes) + '>' @@ -57,7 +57,12 @@ function renderHTML (vnode, path, context) { if (innerHTML) { str += innerHTML } else { - str += children.map((child, i) => renderString(child, context, path + '.' + (isNull(child.key) ? i : child.key))).join('') + str += children.map((child, i) => renderString( + child, + context, + path + '.' + (isNull(child.key) ? i : child.key), + opts + )).join('') } str += '' diff --git a/test/app/index.js b/test/app/index.js index 5774ccb..4a4e4aa 100644 --- a/test/app/index.js +++ b/test/app/index.js @@ -3,6 +3,7 @@ import test from 'tape' import {createApp} from '../../src/app' import {create as h} from '../../src/element' +import trigger from 'trigger-event' test('rendering elements', t => { let el = document.createElement('div') @@ -66,19 +67,6 @@ test('moving elements using keys', t => { t.end() }) -test('emptying the container', t => { - let el = document.createElement('div') - el.innerHTML = '
' - let render = createApp(el) - render() - t.equal( - el.innerHTML, - '', - 'container emptied' - ) - t.end() -}) - test('context should be passed down all elements', t => { let Form = { render ({ props, context }) { @@ -283,3 +271,90 @@ test('rendering and updating null', t => { t.end() }) + +test('rendering in a container with pre-rendered HTML', t => { + let el = document.createElement('div') + document.body.appendChild(el) + + el.innerHTML = '
Meow
' + let render = createApp(el) + render(
Thrr
) + t.equal( + el.innerHTML, + '
Thrr
', + 'inequivalent string updated' + ) + + // Should work fine for all except root element + el.innerHTML = '
Nyan!
' + render = createApp(el) + render(

Nyan!

) + t.equal( + el.innerHTML, + '

Nyan!

', + 're-rendered due to changed tagName' + ) + + document.body.removeChild(el) + t.end() +}) + +test('rerendering custom element with changing props', t => { + let el = document.createElement('div') + document.body.appendChild(el) + const Comp = { + render ({props, path}) { + return ( + woot + ) + } + } + + let render = createApp(el, { reuseMarkup: true }) + + el.innerHTML = '
woot
' + el.children[0].attributes.chck = 1 + el.children[0].children[0].attributes.chck = 2 + + render(
) + + t.equal( + el.children[0].attributes.chck, + 1, + 'should not rerender outer div' + ) + t.equal( + el.children[0].children[0].attributes.chck, + 2, + 'should not rerender inner span' + ) + t.equal( + el.innerHTML, + '
woot
', + 'attributes should be updated when needed' + ) + + document.body.removeChild(el) + t.end() +}) + +test('rendering in a container with pre-rendered HTML and click events', t => { + t.plan(12) + let el = document.createElement('div') + el.innerHTML = '
' + let render = createApp(el) + let a = () => { + t.assert('clicked') + } + let b = () => { + t.assert('clicked') + t.assert('clicked') + } + render(
) + let arr = el.querySelectorAll('button, span') + for (var item of arr) { + trigger(item, 'click') + trigger(item, 'click') + } + t.end() +}) diff --git a/test/app/thunk.js b/test/app/thunk.js index def05f1..7145665 100644 --- a/test/app/thunk.js +++ b/test/app/thunk.js @@ -283,9 +283,9 @@ test('path should stay the same on when thunk is replaced', t => { } } render(
) - render(
) + render(
) render(
) render(
) - render(
) + render(
) t.end() }) diff --git a/test/dom/pool.js b/test/dom/pool.js new file mode 100644 index 0000000..887c6fd --- /dev/null +++ b/test/dom/pool.js @@ -0,0 +1,68 @@ +/* global SVGElement */ +import test from 'tape' +import Pool from '../../src/dom/pool' + +test('storeDomNode', t => { + let node = document.createElement('div') + node.__attr = true + let pool = new Pool() + pool.enableRecycling(true) + + pool.store(node) + + let storedNode = pool.get('div') + + t.equal(storedNode.__attr, true, 'Pool returned same node') + t.end() +}) + +test('storeNestedNodes', t => { + let node = document.createElement('div') + for (let i = 0; i < 10; i++) { + node.appendChild(document.createElement('div')) + } + let pool = new Pool() + pool.enableRecycling(true) + + pool.store(node) + + let childNodesCount = 0 + while (pool._getStorageSizeFor('div') > 0) { + let storedNode = pool.get('div') + childNodesCount += storedNode.childNodes.length + } + + t.equal(childNodesCount, 0, 'Stored nested nodes were flattened and do not have children') + t.end() +}) + +test('getNewDomNode', t => { + let pool = new Pool() + let newNode = pool.get('div') + t.equal(newNode.tagName.toLowerCase(), 'div', 'Pool created and returned div') + + pool.enableRecycling(true) + newNode = pool.get('div') + t.equal(newNode.tagName.toLowerCase(), 'div', 'Pool created and returned div with enabled recycling') + t.end() +}) + +test('getNewSvgNode', t => { + let pool = new Pool() + let newNode = pool.get('circle') + + t.equal(newNode instanceof SVGElement, true, 'Pool created svg element') + t.end() +}) + +test('preallocateDomNodes', t => { + let pool = new Pool() + pool.enableRecycling(true) + pool.preallocate('div', 20) + + t.equal(pool._getStorageSizeFor('div'), 20, 'Preallocated 20 elements') + t.equal(pool.get('div').tagName.toLowerCase(), 'div', 'Stored element was div') + t.equal(pool._getStorageSizeFor('div'), 19, '19 elements left after .get()') + t.end() +}) + diff --git a/test/index.js b/test/index.js index 2a0b29c..bbab0d6 100644 --- a/test/index.js +++ b/test/index.js @@ -3,6 +3,7 @@ import './element/index' import './dom/setAttribute' import './dom/create' import './dom/update' +import './dom/pool' import './diff/attributes' import './diff/node' import './diff/children'