diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 85c848c..8770d4b 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -3,6 +3,7 @@ import * as data from './data' import * as registry from './registry' import * as zip from './zip' +import skeleZip from './zip/skele' import * as log from './log' import * as propNames from './propNames' import Cursor from './vendor/cursor' @@ -16,6 +17,7 @@ export default { data, registry, zip, + skeleZip, log, propNames, internal, diff --git a/packages/core/src/zip/elementZipper.js b/packages/core/src/zip/elementZipper.js index 5636ea6..b3efda9 100644 --- a/packages/core/src/zip/elementZipper.js +++ b/packages/core/src/zip/elementZipper.js @@ -1,6 +1,7 @@ 'use strict' import { makeZipper } from '../zip' +import deprecated from '../log/deprecated' import { Iterable, List, Map } from 'immutable' import * as R from 'ramda' import { @@ -77,7 +78,7 @@ const singleChild = childColl => * * @param config, configuration for the object, currently supports only the `defaultChildPositions` property */ -export default function elementZipper(config) { +function elementZipper(config) { const { defaultChildPositions, makeZipperOverride } = config const dcp = asList(defaultChildPositions) @@ -90,3 +91,8 @@ export default function elementZipper(config) { return ElementZipperType.from.bind(ElementZipperType) } + +export default deprecated( + 'elementZipper is deprecated, use `skeleZip` instead', + elementZipper +) diff --git a/packages/core/src/zip/skele/__tests__/zipperImpl.js b/packages/core/src/zip/skele/__tests__/zipperImpl.js new file mode 100644 index 0000000..7ad03d9 --- /dev/null +++ b/packages/core/src/zip/skele/__tests__/zipperImpl.js @@ -0,0 +1,280 @@ +'use strict' + +import R from 'ramda' +import { fromJS } from 'immutable' +import * as zip from '../..' +import skeleZip from '..' +import * as data from '../../../data' +import * as propNames from '../../../propNames' + +const childCollectionKind = '@@skele/child-collection' + +describe('Skele Zipper', () => { + const singleChild = { + kind: 'parent', + [propNames.children]: 'children', + children: [ + { + kind: 'lvl1', + [propNames.children]: 'children', + children: [ + { + kind: 'lvl2', + }, + ], + }, + ], + } + + it('zipper should correctly navigate up and down', () => { + const zipper = skeleZip(fromJS(singleChild)) + + expect(zip.node(zipper).get('kind')).toEqual('parent') + expect(data.isOfKind(childCollectionKind, zip.node(zip.down(zipper)))).toBe( + true + ) + expect( + R.pipe( + zip.down, + zip.down, + zip.node + )(zipper).get('kind') + ).toEqual('lvl1') + + expect( + data.isOfKind( + childCollectionKind, + + R.pipe( + zip.down, + zip.down, + zip.down, + zip.node + )(zipper) + ) + ).toBe(true) + expect( + R.pipe( + zip.down, + zip.down, + zip.down, + zip.down, + zip.node + )(zipper).get('kind') + ).toEqual('lvl2') + expect( + R.pipe( + zip.down, + zip.down, + zip.down, + zip.down, + zip.down + )(zipper) + ).toBeNull() + expect( + R.pipe( + zip.down, + zip.up, + zip.node + )(zipper).get('kind') + ).toEqual('parent') + expect( + R.pipe( + zip.down, + zip.down, + zip.down, + zip.up, + zip.node + )(zipper).get('kind') + ).toEqual('lvl1') + }) + + const multipleChildren = { + id: 1, + kind: 't', + [propNames.children]: 'children', + children: [ + { + id: 2, + kind: 't', + [propNames.children]: 'children', + children: [ + { + id: 3, + kind: 't', + }, + { + kind: 't', + id: 4, + }, + ], + }, + { + id: 5, + kind: 't', + [propNames.children]: 'children', + children: [ + { + kind: 't', + id: 6, + }, + { + kind: 't', + id: 7, + }, + ], + }, + { + id: 8, + kind: 't', + [propNames.children]: 'children', + children: [ + { + kind: 't', + id: 9, + }, + { + kind: 't', + id: 10, + }, + ], + }, + ], + } + + it('zipper should correctly navigate up down left and right', () => { + const zipper = skeleZip(fromJS(multipleChildren)) + + expect(zip.node(zipper).get('id')).toEqual(1) + expect(data.isOfKind(childCollectionKind, zip.node(zip.down(zipper)))).toBe( + true + ) + expect( + R.pipe( + zip.down, + zip.down, + zip.node + )(zipper).get('id') + ).toEqual(2) + expect( + R.pipe( + zip.down, + zip.down, + zip.right, + zip.node + )(zipper).get('id') + ).toEqual(5) + expect( + R.pipe( + zip.down, + zip.down, + zip.right, + zip.right, + zip.node + )(zipper).get('id') + ).toEqual(8) + expect( + R.pipe( + zip.down, + zip.down, + zip.right, + zip.right, + zip.left, + zip.node + )(zipper).get('id') + ).toEqual(5) + expect( + data.isOfKind( + childCollectionKind, + R.pipe( + zip.down, + zip.down, + zip.right, + zip.right, + zip.left, + zip.up, + zip.node + )(zipper) + ) + ).toBe(true) + expect( + R.pipe( + zip.down, + zip.down, + zip.right, + zip.right, + zip.left, + zip.up, + zip.up, + zip.node + )(zipper).get('id') + ).toEqual(1) + }) + + const multipleChildrenElements = { + id: 1, + kind: 't', + [propNames.children]: ['left', 'right'], + left: [ + { + kind: 't', + id: 2, + }, + ], + right: [ + { + kind: 't', + id: 3, + }, + { + kind: 't', + id: 4, + }, + ], + } + + it('zipper multiple children elements', () => { + const zipper = skeleZip(fromJS(multipleChildrenElements)) + + expect(zip.node(zipper).get('id')).toEqual(1) + expect( + R.pipe( + zip.down, + zip.node + )(zipper).get('propertyName') + ).toEqual('left') + + expect( + R.pipe( + zip.down, + zip.right, + zip.node + )(zipper).get('propertyName') + ).toEqual('right') + + expect( + R.pipe( + zip.down, + zip.down, + zip.node + )(zipper).get('id') + ).toEqual(2) + expect( + R.pipe( + zip.down, + zip.right, + zip.down, + zip.node + )(zipper).get('id') + ).toEqual(3) + expect( + R.pipe( + zip.down, + zip.right, + zip.down, + zip.right, + zip.node + )(zipper).get('id') + ).toEqual(4) + }) +}) diff --git a/packages/core/src/zip/skele/index.js b/packages/core/src/zip/skele/index.js new file mode 100644 index 0000000..2c88cb5 --- /dev/null +++ b/packages/core/src/zip/skele/index.js @@ -0,0 +1,4 @@ +'use strict' +import skeleZip from './zipperImpl' + +export default skeleZip diff --git a/packages/core/src/zip/skele/zipperImpl.js b/packages/core/src/zip/skele/zipperImpl.js new file mode 100644 index 0000000..ec0f009 --- /dev/null +++ b/packages/core/src/zip/skele/zipperImpl.js @@ -0,0 +1,69 @@ +'use strict' + +import { zipper } from '../../zip' +import { Iterable, List, Map } from 'immutable' +import { isOfKind, asList, childPositions } from '../../data' + +const isBranch = element => { + if (isOfKind('@@skele/child-collection', element)) { + return true + } + + const positions = childPositions(element) + + return positions != null && !positions.isEmpty() +} + +const getChildren = element => { + if (isOfKind('@@skele/child-collection', element)) { + return element.get('children').toArray() + } + // at a children collection level + const positions = childPositions(element) + + const children = positions + .reduce( + (children, p) => + element.get(p) + ? children.push(makeChildCollection(p, element.get(p))) + : children, + List() + ) + .toArray() + + return children +} + +const makeChildCollection = (p, children) => + Map({ + kind: '@@skele/child-collection', + propertyName: p, + isSingle: !Iterable.isIndexed(children), + children: asList(children), + }) + +const makeNode = (element, children) => { + if (isOfKind('@@skele/child-collection', element)) { + return element.set('children', List(children)) + } + return children.reduce( + (el, childColl) => + el.set( + childColl.get('propertyName'), + singleChild(childColl) + ? childColl.getIn(['children', 0]) + : childColl.get('children') + ), + element + ) +} + +const singleChild = childColl => + childColl.get('isSingle') && childColl.get('children').count() === 1 + +/** + * Creates a zipper over a Skele state tree. + * + * @param root the root node of the state tree + */ +export default zipper.bind(undefined, isBranch, getChildren, makeNode)