diff --git a/src/index.js b/src/index.js index 3e29260..a9edd25 100644 --- a/src/index.js +++ b/src/index.js @@ -180,26 +180,113 @@ function unmountComponentAtNode(container) { return false; } +/** Children Helpers **/ + +const KEY_SEPARATOR = '.'; +const KEY_SUBSEPARATOR = ':'; +const ESCAPE_REGEX = /[=:]/g; +const ESCAPER_LOOKUP = { + '=': '=0', + ':': '=2' +}; +const USER_KEY_ESCAPE_REGEX = /\/+/g; +function escapeUserProvidedKey(text) { + return ('' + text).replace(USER_KEY_ESCAPE_REGEX, '$&/'); +} -const ARR = []; +function getKey(component, i) { + if (typeof component === 'object' && component != null && component.key) { + return '$' + ('' + component.key).replace(ESCAPE_REGEX, (match) => ESCAPER_LOOKUP[match]); + } + + return String(i); +} + +function cloneAndReplaceKey(oldElement, newKey) { + return; +} + +function iterateChildren(children, callback, name) { + let type = typeof children; + + if (type === 'undefined' || type === 'boolean') { + children = null; + } -// This API is completely unnecessary for Preact, so it's basically passthrough. + if (children === null || type === 'string' || type === 'number' || type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE) { + return callback(children, name === '' ? KEY_SEPARATOR + getKey(children, 0) : name); + } + + let child; + let nextName; + let nextNamePrefix = name === '' ? KEY_SEPARATOR : name + KEY_SUBSEPARATOR; + + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + child = children[i]; + nextName = nextNamePrefix + getKey(child, i); + iterateChildren(child, callback, nextName); + } + } + else if (type === 'object'){ + throw new Error('can not iterate over object children'); + } +} + + +function identity (el) { + return el; +} + +function mapChildren (children, fn, result, prefix, count, ctx) { + if (children == null) { + return children; + } + + iterateChildren(children, (child, childKey) => { + let mappedChild = fn.call(ctx, child, count++); + + if (Array.isArray(mappedChild)) { + mapChildren(mappedChild, fn, result, childKey, count, ctx); + } + else if (mappedChild) { + if (isValidElement(mappedChild)) { + const newKey = prefix + (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey(mappedChild.key) + '/' : '') + childKey; + result.push(cloneElement(mappedChild, {key: newKey})); + } + else { + result.push(mappedChild); + } + } + }, prefix); +} + +// This API is completely unnecessary for Preact, +// but a lot of libraries rely on it to function properly. let Children = { map(children, fn, ctx) { - if (children == null) return null; - children = Children.toArray(children); - if (ctx && ctx!==children) fn = fn.bind(ctx); - return children.map(fn); + let res = []; + if (children == null) { + return res; + } + + mapChildren(children, fn, res, '', 0, ctx); + + return res; }, forEach(children, fn, ctx) { - if (children == null) return null; - children = Children.toArray(children); - if (ctx && ctx!==children) fn = fn.bind(ctx); - children.forEach(fn); + if (children == null) { + return children; + } + mapChildren(children, fn, [], '', 0, ctx); }, count(children) { - return children && children.length || 0; + let count = 0; + Children.forEach(children, () => { + count++; + }); + return count; }, only(children) { children = Children.toArray(children); @@ -207,12 +294,14 @@ let Children = { return children[0]; }, toArray(children) { - if (children == null) return []; - return ARR.concat(children); + let res = []; + + mapChildren(children, identity, res, '', 0, null); + + return res; } }; - /** Track current render() component for ref assignment */ let currentComponent; @@ -439,6 +528,7 @@ function collateMixins(mixins) { return keyed; } +const ARR = []; // apply a mapping of Arrays of mixin methods to a component prototype function applyMixins(proto, mixins) { diff --git a/test/children.js b/test/children.js new file mode 100644 index 0000000..ce611f7 --- /dev/null +++ b/test/children.js @@ -0,0 +1,728 @@ +import React, { render, createClass, createElement, cloneElement, Component, PropTypes, unstable_renderSubtreeIntoContainer } from '../src'; + +describe('Children', () => { + it('should support identity for simple', () => { + let context = {}; + let callback = sinon.spy(function (kid, index) { + expect(this).to.eql(context); + return kid; + }); + + let simpleKid = ; + + // First pass children into a component to fully simulate what happens when + // using structures that arrive from transforms. + + let instance =
{simpleKid}
; + React.Children.forEach(instance.props.children, callback, context); + expect(callback).to.have.been.calledWithExactly(simpleKid, 0); + callback.reset(); + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + expect(callback).to.have.been.calledWithExactly(simpleKid, 0); + expect(mappedChildren[0]).to.eql(); + }); + + it('should treat single arrayless child as being in array', () => { + let context = {}; + let callback = sinon.spy(function(kid, index) { + expect(this).to.equal(context); + return kid; + }); + + let simpleKid = ; + let instance =
{simpleKid}
; + React.Children.forEach(instance.props.children, callback, context); + expect(callback).to.have.been.calledWithExactly(simpleKid, 0); + callback.reset(); + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + expect(callback).to.have.been.calledWithExactly(simpleKid, 0); + expect(mappedChildren[0]).to.eql(); + }); + + it('should treat single child in array as expected', () => { + let context = {}; + let callback = sinon.spy(function(kid, index) { + expect(this).to.equal(context); + return kid; + }); + + let simpleKid = ; + let instance =
{[simpleKid]}
; + React.Children.forEach(instance.props.children, callback, context); + expect(callback.args[0]).to.be.eql([simpleKid, 0]); + callback.reset(); + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + + expect(callback.args[0]).to.be.eql([simpleKid, 0]); + expect(mappedChildren[0]).to.eql(); + }); + + it('should be called for each child', () => { + let zero =
; + let one = null; + let two =
; + let three = null; + let four =
; + let context = {}; + + let callback = sinon.spy(function(kid) { + expect(this).to.equal(context); + return kid; + }); + + let instance = ( +
+ {zero} + {one} + {two} + {three} + {four} +
+ ); + + function assertCalls() { + expect(callback.args[0]).to.eql([zero, 0]); + expect(callback.args[1]).to.eql(['' /* one */, 1]); + expect(callback.args[2]).to.eql([two, 2]); + expect(callback.args[3]).to.eql(['' /* three */, 3]); + expect(callback.args[4]).to.eql([four, 4]); + callback.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + assertCalls(); + expect(mappedChildren).to.eql([ +
, +
, +
+ ]); + }); + + it('should be called for each child in nested structure', () => { + let zero =
; + let one = null; + let two =
; + let three = null; + let four =
; + let five =
; + + let context = {}; + let callback = sinon.spy((kid) => { + return kid; + }); + + let instance = ( +
+ {[[zero, one, two], [three, four], five]} +
+ ); + + function assertCalls() { + expect(callback.callCount).to.equal(6); + expect(callback.args[0]).to.eql([zero, 0]); + expect(callback.args[1]).to.eql(['' /* one */, 1]); + expect(callback.args[2]).to.eql([two, 2]); + expect(callback.args[3]).to.eql(['' /* three */, 3]); + expect(callback.args[4]).to.eql([four, 4]); + expect(callback.args[5]).to.eql([five, 5]); + callback.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + assertCalls(); + // will not work, as preact normalizes the children array to [a,b,c,d] + /* expect(mappedChildren).to.eql([ +
, +
, +
, +
+ ]); */ + }); + + it('should retain key across two mappings', () => { + let zeroForceKey =
; + let oneForceKey =
; + let context = {}; + let callback = sinon.spy(function(kid) { + expect(this).to.equal(context); + return kid; + }); + + let forcedKeys = ( +
+ {zeroForceKey} + {oneForceKey} +
+ ); + + function assertCalls() { + expect(callback.args[0]).to.eql([zeroForceKey, 0]); + expect(callback.args[1]).to.eql([oneForceKey, 1]); + callback.reset(); + } + + React.Children.forEach(forcedKeys.props.children, callback, context); + assertCalls(); + + let mappedChildren = React.Children.map( + forcedKeys.props.children, + callback, + context, + ); + assertCalls(); + expect(mappedChildren).to.eql([ +
, +
+ ]); + }); + + it.skip('should be called for each child in an iterable without keys', () => { + let threeDivIterable = { + '@@iterator'() { + let i = 0; + return { + next() { + if (i++ < 3) { + return {value:
, done: false}; + } + return {value: undefined, done: true}; + + } + }; + } + }; + + let context = {}; + let callback = sinon.spy(function(kid) { + expect(this).to.equal(context); + return kid; + }); + + let instance = ( +
+ {threeDivIterable} +
+ ); + + function assertCalls() { + expect(callback.callCount).to.equal(3); + expect(callback.args[0]).to.eql([
, 0]); + expect(callback.args[1]).to.eql([
, 1]); + expect(callback.args[2]).to.eql([
, 2]); + callback.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + assertCalls(); + expect(mappedChildren).to.eql([ +
, +
, +
+ ]); + }); + + it.skip('should be called for each child in an iterable with keys', () => { + let threeDivIterable = { + '@@iterator'() { + let i = 0; + return { + next() { + if (i++ < 3) { + return {value:
, done: false}; + } + return {value: undefined, done: true}; + + } + }; + } + }; + + let context = {}; + let callback = sinon.spy(function(kid) { + expect(this).to.equal(context); + return kid; + }); + + let instance = ( +
+ {threeDivIterable} +
+ ); + + function assertCalls() { + expect(callback.callCount).to.equal(3); + expect(callback.args[0]).to.eql([
, 0]); + expect(callback.args[1]).to.eql([
, 1]); + expect(callback.args[2]).to.eql([
, 2]); + callback.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + assertCalls(); + expect(mappedChildren).to.eql([ +
, +
, +
+ ]); + }); + + it('should allow extension of native prototypes', () => { + /*eslint-disable no-extend-native */ + String.prototype.key = 'react'; + /*eslint-enable no-extend-native */ + + let instance = ( +
+ {'a'} +
+ ); + + let context = {}; + let callback = sinon.spy(function(kid) { + expect(this).to.equal(context); + return kid; + }); + + function assertCalls() { + expect(callback.callCount).to.equal(1, 0); + expect(callback.args[0]).to.eql(['a', 0]); + callback.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + let mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + assertCalls(); + expect(mappedChildren).to.eql(['a']); + + delete String.prototype.key; + }); + + it('should pass key to returned component', () => { + let mapFn = function(kid, index) { + return
{kid}
; + }; + + let simpleKid = ; + + let instance =
{simpleKid}
; + let mappedChildren = React.Children.map(instance.props.children, mapFn); + + expect(React.Children.count(mappedChildren)).to.equal(1); + expect(mappedChildren[0]).not.to.equal(simpleKid); + expect(mappedChildren[0].props.children).to.eql([simpleKid]); + expect(mappedChildren[0].key).to.equal('.$simple'); + }); + + it('should invoke callback with the right context', () => { + let lastContext; + let callback = function(kid, index) { + lastContext = this; + return this; + }; + + // TODO: Use an object to test, after non-object fragments has fully landed. + let scopeTester = 'scope tester'; + + let simpleKid = ; + let instance =
{simpleKid}
; + React.Children.forEach(instance.props.children, callback, scopeTester); + expect(lastContext).to.equal(scopeTester); + + let mappedChildren = React.Children.map( + instance.props.children, + callback, + scopeTester, + ); + + expect(React.Children.count(mappedChildren)).to.equal(1); + expect(mappedChildren[0]).to.equal(scopeTester); + }); + + it('should be called for each child', () => { + let zero =
; + let one = null; + let two =
; + let three = null; + let four =
; + + let mapped = [ +
, // Key should be joined to obj key + null, // Key should be added even if we don't supply it! +
, // Key should be added even if not supplied! + , // Map from null to something. +
+ ]; + let callback = sinon.spy((kid, index) => { + return mapped[index]; + }); + + let instance = ( +
+ {zero} + {one} + {two} + {three} + {four} +
+ ); + + React.Children.forEach(instance.props.children, callback); + expect(callback.args[0]).to.eql([zero, 0]); + expect(callback.args[1]).to.eql(['' /* one */ , 1]); // null get converted to '' from preact + expect(callback.args[2]).to.eql([two, 2]); + expect(callback.args[3]).to.eql(['' /* three */, 3]); + expect(callback.args[4]).to.eql([four, 4]); + callback.reset(); + + let mappedChildren = React.Children.map(instance.props.children, callback); + expect(callback.callCount).to.equal(5); + expect(React.Children.count(mappedChildren)).to.equal(4); + // Keys default to indices. + expect([ + mappedChildren[0].key, + mappedChildren[1].key, + mappedChildren[2].key, + mappedChildren[3].key + ]).to.eql(['giraffe/.$keyZero', '.$keyTwo', '.3', '.$keyFour']); + + expect(callback.args[0]).to.eql([zero, 0]); + expect(callback.args[1]).to.eql(['' /* one */, 1]); + expect(callback.args[2]).to.eql([two, 2]); + expect(callback.args[3]).to.eql(['' /* three */, 3]); + expect(callback.args[4]).to.eql([four, 4]); + + expect(mappedChildren[0]).to.eql(
); + expect(mappedChildren[1]).to.eql(
); + expect(mappedChildren[2]).to.eql(); + expect(mappedChildren[3]).to.eql(
); + }); + + it('should be called for each child in nested structure', () => { + let zero =
; + let one = null; + let two =
; + let three = null; + let four =
; + let five =
; + + let zeroMapped =
; // Key should be overridden + let twoMapped =
; // Key should be added even if not supplied! + let fourMapped =
; + let fiveMapped =
; + + let callback = sinon.spy((kid) => { + switch (kid) { + case zero: + return zeroMapped; + case two: + return twoMapped; + case four: + return fourMapped; + case five: + return fiveMapped; + default: + return kid; + } + }); + + let frag = [[zero, one, two], [three, four], five]; + let instance =
{[frag]}
; + + React.Children.forEach(instance.props.children, callback); + expect(callback.callCount).to.equal(6); + expect(callback.args[0]).to.eql([zero, 0]); + expect(callback.args[1]).to.eql(['' /* one */, 1]); + expect(callback.args[2]).to.eql([two, 2]); + expect(callback.args[3]).to.eql(['' /* three */, 3]); + expect(callback.args[4]).to.eql([four, 4]); + expect(callback.args[5]).to.eql([five, 5]); + callback.reset(); + + let mappedChildren = React.Children.map(instance.props.children, callback); + expect(callback.callCount).to.equal(6); + expect(callback.args[0]).to.eql([zero, 0]); + expect(callback.args[1]).to.eql(['' /* one */, 1]); + expect(callback.args[2]).to.eql([two, 2]); + expect(callback.args[3]).to.eql(['' /* three */, 3]); + expect(callback.args[4]).to.eql([four, 4]); + expect(callback.args[5]).to.eql([five, 5]); + + expect(React.Children.count(mappedChildren)).to.equal(4); + // Keys default to indices. + + // will not work, as preact normalizes the children array to [a,b,c,d] + /* expect([ + mappedChildren[0].key, + mappedChildren[1].key, + mappedChildren[2].key, + mappedChildren[3].key + ]).to.eql([ + 'giraffe/.0:0:$keyZero', + '.0:0:$keyTwo', + '.0:1:$keyFour', + '.0:$keyFive' + ]); */ + + expect(mappedChildren[0]).to.eql(
); + expect(mappedChildren[1]).to.eql(
); + expect(mappedChildren[2]).to.eql(
); + expect(mappedChildren[3]).to.eql(
); + }); + + it('should retain key across two mappings', () => { + let zeroForceKey =
; + let oneForceKey =
; + + // Key should be joined to object key + let zeroForceKeyMapped =
; + // Key should be added even if we don't supply it! + let oneForceKeyMapped =
; + + let mapFn = function(kid, index) { + return index === 0 ? zeroForceKeyMapped : oneForceKeyMapped; + }; + + let forcedKeys = ( +
+ {zeroForceKey} + {oneForceKey} +
+ ); + + let expectedForcedKeys = ['giraffe/.$keyZero', '.$keyOne']; + let mappedChildrenForcedKeys = React.Children.map( + forcedKeys.props.children, + mapFn, + ); + let mappedForcedKeys = mappedChildrenForcedKeys.map(c => c.key); + expect(mappedForcedKeys).to.eql(expectedForcedKeys); + + let expectedRemappedForcedKeys = [ + 'giraffe/.$giraffe/.$keyZero', + '.$.$keyOne' + ]; + let remappedChildrenForcedKeys = React.Children.map( + mappedChildrenForcedKeys, + mapFn, + ); + expect(remappedChildrenForcedKeys.map(c => c.key)).to.eql( + expectedRemappedForcedKeys, + ); + }); + + it('should not throw if key provided is a dupe with array key', () => { + let zero =
; + let one =
; + + let mapFn = function() { + return null; + }; + + let instance = ( +
+ {zero} + {one} +
+ ); + + expect(() => { + React.Children.map(instance.props.children, mapFn); + }).not.to.throw(); + }); + + it('should use the same key for a cloned element', () => { + let instance = ( +
+
+
+ ); + + let mapped = React.Children.map( + instance.props.children, + element => element, + ); + + let mappedWithClone = React.Children.map( + instance.props.children, + element => React.cloneElement(element) + ); + + expect(mapped[0].key).to.equal(mappedWithClone[0].key); + }); + + it('should use the same key for a cloned element with key', () => { + let instance = ( +
+
+
+ ); + + let mapped = React.Children.map( + instance.props.children, + element => element + ); + + let mappedWithClone = React.Children.map( + instance.props.children, element => React.cloneElement(element, {key: 'unique'}) + ); + + expect(mapped[0].key).to.equal(mappedWithClone[0].key); + }); + + it('should return 0 for null children', () => { + let numberOfChildren = React.Children.count(null); + expect(numberOfChildren).to.equal(0); + }); + + it('should return 0 for undefined children', () => { + let numberOfChildren = React.Children.count(undefined); + expect(numberOfChildren).to.equal(0); + }); + + it('should return 1 for single child', () => { + let simpleKid = ; + let instance =
{simpleKid}
; + let numberOfChildren = React.Children.count(instance.props.children); + expect(numberOfChildren).to.equal(1); + }); + + it('should count the number of children in flat structure', () => { + let zero =
; + let one = null; + let two =
; + let three = null; + let four =
; + + let instance = ( +
+ {zero} + {one} + {two} + {three} + {four} +
+ ); + let numberOfChildren = React.Children.count(instance.props.children); + expect(numberOfChildren).to.equal(5); + }); + + it('should count the number of children in nested structure', () => { + let zero =
; + let one = null; + let two =
; + let three = null; + let four =
; + let five =
; + + let instance = ( +
+ {[[[zero, one, two], [three, four], five], null]} +
+ ); + let numberOfChildren = React.Children.count(instance.props.children); + expect(numberOfChildren).to.equal(7); + }); + + it('should flatten children to an array', () => { + expect(React.Children.toArray(undefined)).to.eql([]); + expect(React.Children.toArray(null)).to.eql([]); + + expect(React.Children.toArray(
).length).to.equal(1); + expect(React.Children.toArray([
]).length).to.equal(1); + expect(React.Children.toArray(
)[0].key).to.equal( + React.Children.toArray([
])[0].key, + ); + + let flattened = React.Children.toArray([ + [
,
,
], + [
,
,
] + ]); + + expect(flattened.length).to.equal(6); + expect(flattened[1].key).to.contain('banana'); + expect(flattened[3].key).to.contain('banana'); + expect(flattened[1].key).not.to.equal(flattened[3].key); + + let reversed = React.Children.toArray([ + [
,
,
], + [
,
,
] + ]); + expect(flattened[0].key).to.equal(reversed[2].key); + expect(flattened[1].key).to.equal(reversed[1].key); + expect(flattened[2].key).to.equal(reversed[0].key); + expect(flattened[3].key).to.equal(reversed[5].key); + expect(flattened[4].key).to.equal(reversed[4].key); + expect(flattened[5].key).to.equal(reversed[3].key); + + // null/undefined/bool are all omitted + expect(React.Children.toArray([1, 'two', null, undefined, true])).to.eql([ + 1, + 'two' + ]); + }); + + it('should escape keys', () => { + let zero =
; + let one =
; + let instance = ( +
+ {zero} + {one} +
+ ); + let mappedChildren = React.Children.map( + instance.props.children, + kid => kid, + ); + expect(mappedChildren).to.eql([ +
, +
+ ]); + }); +}); diff --git a/test/index.js b/test/index.js index d37f266..34e9346 100644 --- a/test/index.js +++ b/test/index.js @@ -54,9 +54,9 @@ describe('preact-compat', () => { render(
dynamic content
, root); expect(root) - .to.have.property('textContent') - .that.is.a('string') - .that.equals('dynamic content'); + .to.have.property('textContent') + .that.is.a('string') + .that.equals('dynamic content'); }); it('should support defaultValue', () => { @@ -155,7 +155,7 @@ describe('preact-compat', () => { expect(def.mixins[1].bar).to.have.been.calledOnce.and.calledAfter(def.mixins[0].bar); let props = {}, - state = {}; + state = {}; inst.componentWillMount(props, state); expect(def.mixins[1].componentWillMount) .to.have.been.calledOnce @@ -180,7 +180,7 @@ describe('preact-compat', () => { it('should normalize vnodes', () => { let vnode = ; // using typeof Symbol here injects a polyfill, which ruins the test. we'll hardcode the non-symbol value for now. - let $$typeof = 0xeac7; + let $$typeof = 0xeac7; expect(vnode).to.have.property('$$typeof', $$typeof); expect(vnode).to.have.property('type', 'div'); expect(vnode).to.have.property('props').that.is.an('object');