Skip to content
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ You can provide a second argument to arrayToTree with configuration options. Rig

- `id`: key of the id field of the item
- `parentId`: key of the parent's id field of the item
- `maintainStructure`: boolean to keep the current structure of your data. Not put it in a `data` field

Example:

Expand All @@ -71,7 +72,7 @@ const tree = arrayToTree([
{ num: '1941', ref: '418', custom: 'de' },
{ num: '1', ref: '418', custom: 'ZZZz' },
{ num: '418', ref: null, custom: 'ü'},
], { id: 'num', parentId: 'ref' })
], { id: 'num', parentId: 'ref', maintainStructure: false })
```

Which produces:
Expand All @@ -88,6 +89,32 @@ Which produces:
]
```

Example:

```js
const tree = arrayToTree([
{ num: '4', ref: null, custom: 'abc' },
{ num: '31', ref: '4', custom: '12' },
{ num: '1941', ref: '418', custom: 'de' },
{ num: '1', ref: '418', custom: 'ZZZz' },
{ num: '418', ref: null, custom: 'ü'},
], { id: 'num', parentId: 'ref', maintainStructure: true })
```

Which produces:

```js
[
{ num: '4', ref: null, custom: 'abc', children: [
{ num: '31', ref: '4', custom: '12', children: [] },
] },
{ num: '418', ref: null, custom: 'ü', children: [
{ num: '1941', ref: '418', custom: 'de', children: [] },
{ num: '1', ref: '418', custom: 'ZZZz', children: [] },
] },
]
```

## TypeScript

This project includes types, just import the module as usual:
Expand Down
77 changes: 76 additions & 1 deletion src/arrayToTree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('arrayToTree', () => {
{ num: '1', ref: '418', custom: 'ZZZz' },
{ num: '418', ref: null, custom: 'ü' },
] as any),
{ id: 'num', parentId: 'ref' },
{ id: 'num', parentId: 'ref', maintainStructure: false },
)).to.deep.equal([
{
data: { num: '4', ref: null, custom: 'abc' }, children: [
Expand Down Expand Up @@ -72,6 +72,81 @@ describe('arrayToTree', () => {
])
})

it('should work with nested objects structure maintained', () => {
expect(arrayToTree(
([
{ id: '4', parentId: null, custom: 'abc' },
{ id: '31', parentId: '4', custom: '12' },
{ id: '1941', parentId: '418', custom: 'de' },
{ id: '1', parentId: '418', custom: 'ZZZz' },
{ id: '418', parentId: null, custom: 'ü' },
] as any),
{ id: 'id', parentId: 'parentId', maintainStructure: true },
)).to.deep.equal([
{
id: '4', parentId: null, custom: 'abc', children: [
{ id: '31', parentId: '4', custom: '12', children: [] },
],
},
{
id: '418', parentId: null, custom: 'ü', children: [
{ id: '1941', parentId: '418', custom: 'de', children: [] },
{ id: '1', parentId: '418', custom: 'ZZZz', children: [] },
],
},
])
})

it('should work with nested objects and custom keys structure maintained', () => {
expect(arrayToTree(
([
{ num: '4', ref: null, custom: 'abc' },
{ num: '31', ref: '4', custom: '12' },
{ num: '1941', ref: '418', custom: 'de' },
{ num: '1', ref: '418', custom: 'ZZZz' },
{ num: '418', ref: null, custom: 'ü' },
] as any),
{ id: 'num', parentId: 'ref', maintainStructure: true },
)).to.deep.equal([
{
num: '4', ref: null, custom: 'abc', children: [
{ num: '31', ref: '4', custom: '12', children: [] },
],
},
{
num: '418', ref: null, custom: 'ü', children: [
{ num: '1941', ref: '418', custom: 'de', children: [] },
{ num: '1', ref: '418', custom: 'ZZZz', children: [] },
],
},
])
})

it('should ignore objects if parentId does not exist structure maintained', () => {
expect(arrayToTree(
([
{ id: '4', parentId: null, custom: 'abc' },
{ id: '31', parentId: '4', custom: '12' },
{ id: '1941', parentId: '418', custom: 'de' },
{ id: '1', parentId: '418', custom: 'ZZZz' },
{ id: '418', parentId: null, custom: 'ü' },
] as any),
{ id: 'id', parentId: 'parentId', maintainStructure: true },
)).to.deep.equal([
{
id: '4', parentId: null, custom: 'abc', children: [
{ id: '31', parentId: '4', custom: '12', children: [] },
],
},
{
id: '418', parentId: null, custom: 'ü', children: [
{ id: '1941', parentId: '418', custom: 'de', children: [] },
{ id: '1', parentId: '418', custom: 'ZZZz', children: [] },
],
},
])
})

it('should work with empty inputs', () => {
expect(arrayToTree([])).to.deep.equal([])
})
Expand Down
23 changes: 18 additions & 5 deletions src/arrayToTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ export interface Item {
}

export interface TreeItem {
data: Item | null
[key: string]: any,
children: TreeItem[]
}

export interface Config {
id: string,
parentId: string,
maintainStructure: boolean,
}

/**
* Unflattens an array to a tree with runtime O(n)
*/
export function arrayToTree (items: Item[], config: Config = { id: 'id', parentId: 'parentId' }): TreeItem[] {
export function arrayToTree (items: Item[], config: Config = { id: 'id', parentId: 'parentId', maintainStructure: false }): TreeItem[] {
// the resulting unflattened tree
const rootItems: TreeItem[] = []

Expand All @@ -35,11 +36,19 @@ export function arrayToTree (items: Item[], config: Config = { id: 'id', parentI
// look whether item already exists in the lookup table
if (!Object.prototype.hasOwnProperty.call(lookup, itemId)) {
// item is not yet there, so add a preliminary item (its data will be added later)
lookup[itemId] = { data: null, children: [] }
if(config.maintainStructure){
lookup[itemId] = { children: [] }
} else {
lookup[itemId] = { data: null, children: [] }
}
}

// add the current item's data to the item in the lookup table
lookup[itemId].data = item
if(config.maintainStructure) {
lookup[itemId] = {children: lookup[itemId].children, ...item}
} else {
lookup[itemId].data = item
}

const TreeItem = lookup[itemId]

Expand All @@ -52,7 +61,11 @@ export function arrayToTree (items: Item[], config: Config = { id: 'id', parentI
// look whether the parent already exists in the lookup table
if (!Object.prototype.hasOwnProperty.call(lookup, parentId)) {
// parent is not yet there, so add a preliminary parent (its data will be added later)
lookup[parentId] = { data: null, children: [] }
if(config.maintainStructure){
lookup[parentId] = { children: [] }
} else {
lookup[parentId] = { data: null, children: [] }
}
}

// add the current item to the parent
Expand Down