3

I've have a complex data structure with multiple nested arrays in place.

Below is the current structure

var contentData = {
  data: {
    content: [
      {
        type: "column",
        sections: [
          {
            sub: [
              {
                type: "heading-1",
                text: "Heading Text"
              }
            ]
          }
        ]
      },
      {
        type: "acc-item",
        sections: [
          {
            sub: [
              {
                type: "heading-1",
                text: "Heading Text"
              },
              {
                type: "ordered-item",
                text: "Item 1"
              },
              {
                type: "unordered-item",
                text: "Item 2"
              }
            ]
          }
        ]
      },
      {
        type: "acc-item",
        sections: [
          {
            sub: [
              {
                type: "heading-1",
                text: "Heading Text 2"
              }
            ]
          }
        ]
      }
    ]
  }
}

So What I wanted is,

  1. I wanted to group all the ordered-item & unordered-item into a new object like {type: 'list', items:[all list items]}.

  2. I need to extract all items which are inside sub and push it to new object embedded and it should placed in the root level like below,

    {type:"acc-item",embedded:[{type:"heading-1",text:"Heading Text 2"}]};

So What I've done so far,

I can able to group acc-item, but not the ordered-item & unordered-item.

So my final expected result should like this,

[{
  "type": "column",
  "embedded": [
    {
      "type": "heading-1",
      "text": "Heading Text"
    }
  ]
},
{
  "type": "acc-group",
  "items": [
    {
      "type": "acc-item",
      "embedded": [
        {
          "type": "heading-1",
          "text": "Heading Text"
        },
        {
          "type": "list",
          "items": [
            {
              "type": "ordered-item",
              "text": "Item 1"
            },
            {
              "type": "unordered-item",
              "text": "Item 2" 
            }
          ]
        }
      ]
    },
    {
      "type": "acc-item",
      "embedded": [
        {
          "type": "heading-1",
          "text": "Heading Text 2"
        }
      ]
    }
  ]
}]

Below is my code,

var group,contentData={data:{content:[{type:"column",sections:[{sub:[{type:"heading-1",text:"Heading Text"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text"},{type:"ordered-item",text:"Item 1"},{type:"unordered-item",text:"Item 2"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text 2"}]}]}]}},types=[["list",["ordered-item","unordered-item"]],["accordion",["acc-item"]]];

var result = contentData.data.content.reduce((r, o) => {
  var type = (types.find(({ 1: values }) => values.indexOf(o.type) > -1)|| {})[0];
  if (!type) {
    r.push(o);
    group = undefined;
    return r;
  }
  if (!group || group.type !== type) {
    group = { type, items: [] };
    r.push(group);
  }
  group.items.push(o);
  return r;
}, []);

document.body.innerHTML = '<pre>' + JSON.stringify(result, null, '  ') + '</pre>';
user007
  • 189
  • 2
  • 13

2 Answers2

1

You could store the last items array as well as the last embedded array and use them until a column type is found.

var contentData = { data: { content: [{ type: "column", sections: [{ sub: [{ type: "heading-1", text: "Heading Text" }] }] }, { type: "acc-item", sections: [{ sub: [{ type: "heading-1", text: "Heading Text" }, { type: "ordered-item", text: "Item 1" }, { type: "unordered-item", text: "Item 2" }] }] }, { type: "acc-item", sections: [{ sub: [{ type: "heading-1", text: "Heading Text 2" }] }] }] } },
    list = ["ordered-item", "unordered-item"],
    lastItems, lastEmbedded,
    result = contentData.data.content.reduce((r, { type, sections }) => {
        if (type === 'column') {
            r.push({ type, embedded: sections.reduce((q, { sub }) => q.concat(sub), []) });
            lastItems = undefined;
            lastEmbedded = undefined;
            return r;
        }
        if (!lastItems) r.push({ type: "acc-group", items: lastItems = [] });
        lastItems.push(...sections.map(({ sub }) => ({
            type,
            embedded: sub.reduce((q, o) => {
                if (list.includes(o.type)) {
                    if (!lastEmbedded) q.push({ type: 'list', items: lastEmbedded = [] });
                    lastEmbedded.push(o);
                } else {
                    q.push(o);
                    lastEmbedded = undefined;
                }
                return q;
            }, [])
        })));
        return r;
    }, []);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • one issue, `"ordered-item", "unordered-item"` is not grouping inside columns – user007 May 28 '19 at 08:57
  • Can you use this `var contentData={data:{content:[{type:"column",sections:[{sub:[{type:"heading-1",text:"Heading Text"},{type:"ordered-item",text:"Item 1"},{type:"unordered-item",text:"Item 2"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text"},{type:"ordered-item",text:"Item 1"},{type:"unordered-item",text:"Item 2"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text 2"}]}]},{type:"column",sections:[{sub:[{type:"heading-1",text:"Heading Text"},{type:"ordered-item",text:"Item 1"},{type:"unordered-item",text:"Item 2"}]}]}]}};` – user007 May 28 '19 at 08:59
  • how shold look this as result? – Nina Scholz May 28 '19 at 09:05
  • list is not grouping inside column, so expected result `[{type:"column",embedded:[{type:"heading-1",text:"Heading Text"},{type:"list",items:[{type:"ordered-item",text:"Item 1"},{type:"unordered-item",text:"Item 2"}]}]}];` – user007 May 28 '19 at 09:19
0

The Array.prototype and Object.prototype methods are perfect for this kind of thing.

And you're right that this is some complicated kind of logic.

I would suggest that you definitely need some unit tests for this, and try break in to separate pieces.

Here's how I'm thinking I'd do it.

1. Group By the type to create your groups..

I'm actually creating a more generic solution that you've asked for here. That is, I'm not just grouping the 'acc-item', but everything.

I did a quick search for 'array group by javascript' and it gives us this answer which suggests using Array.reduce, so let's do that.

    const groupedData = contentData.data.content.reduce((acc, cur) => {
        //Check if this indexed array already exists, if not create it. 
        const currentArray = (acc[`${cur.type}-group`] && acc[`${cur.type}-group`].items) || [];

        return {
          ...acc,
          [`${cur.type}-group`]: {
              type: `${cur.type}-group`, 
              items: [...currentArray, cur]
          }
        }
    }, {}); 

2. Now for each of those items, we need to look at their subs, and group just the list items.

To do this, we basically want to find all the `item -> sections -> sub -> types and filter them into two arrays. A quick google on how to create two arrays using a filter gives me this answer.

First though, we need to flatten that sections-> subs thing, so lets just do that.

function flattenSectionsAndSubs(item) {
    return {
        type: item.type, 
        subs: item.sections.reduce((acc, cur) => ([...acc, ...cur.sub]), [])
    }; 
}

And I'll just copy paste that partition function in:

function partition(array, isValid) {
  return array.reduce(([pass, fail], elem) => {
    return isValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]];
  }, [[], []]);
}


    const listTypes = ['ordered-item', 'unordered-item']; 
    function createEmbeddedFromItem(item) {
        const [lists, nonLists] = partition(item.subs, (v) => listTypes.includes(v.type); 

      return {
         type: item.type, 
         embedded: [
             ...nonLists, 
             {
                type: "list", 
                items: lists
             }
         ]
      }
    }

Putting this all together and we get.

const contentData = {
  data: {
    content: [{
        type: "column",
        sections: [{
          sub: [{
            type: "heading-1",
            text: "Heading Text"
          }]
        }]
      },
      {
        type: "acc-item",
        sections: [{
          sub: [{
              type: "heading-1",
              text: "Heading Text"
            },
            {
              type: "ordered-item",
              text: "Item 1"
            },
            {
              type: "unordered-item",
              text: "Item 2"
            }
          ]
        }]
      },
      {
        type: "acc-item",
        sections: [{
          sub: [{
            type: "heading-1",
            text: "Heading Text 2"
          }]
        }]
      }
    ]
  }
}


function partition(array, isValid) {
  return array.reduce(([pass, fail], elem) => {
    return isValid(elem) ? [
      [...pass, elem], fail
    ] : [pass, [...fail, elem]];
  }, [
    [],
    []
  ]);
}


function flattenSectionsAndSubs(item) {
  return {
    type: item.type,
    subs: item.sections.reduce((acc, cur) => ([...acc, ...cur.sub]), [])
  };
}

const listTypes = ['ordered-item', 'unordered-item'];

function createEmbeddedFromItem(item) {
  const [lists, nonLists] = partition(item.subs, (v) => listTypes.includes(v.type));

    return {
      type: item.type,
      embedded: [
        ...nonLists,
        {
          type: "list",
          items: lists
        }
      ]
    }
  }


  const groupedData = contentData.data.content.reduce((acc, cur) => {
    //Check if this indexed array already exists, if not create it. 
    const currentArray = (acc[`${cur.type}-group`] && acc[`${cur.type}-group`].items) || [];

    const flattenedItem = flattenSectionsAndSubs(cur);
    const embeddedItem = createEmbeddedFromItem(flattenedItem);
    return {
      ...acc,
      [`${cur.type}-group`]: {
          type: `${cur.type}-group`, 
          items: [...currentArray, embeddedItem]
      }
    }
  }, {});

  console.log(groupedData);

Now this doesn't exactly match what you've asked for - but it should probably work.

You can add your own bits into only add a list item, if the array isn't empty, and to stop the column from being in its own group.

The thing is - tbh it seems like a little bit of a red flag that you would create an array of items that don't having matching structures, which is why I've done it this way.

dwjohnston
  • 11,163
  • 32
  • 99
  • 194
  • in `flattenSectionsAndSubs` .flat dosent work in IE – user007 May 28 '19 at 03:12
  • See this answer here: https://stackoverflow.com/questions/38696119/es6-spread-syntax-ie-not-supported You might want to use a polyfill, or use babel to compile your production code, or change the syntax slightly. – dwjohnston May 28 '19 at 03:16
  • I've copied the code here, so that it's compiled with babel - see that it works there: https://codepen.io/dwjohnston/pen/KLBwza?editors=1111 – dwjohnston May 28 '19 at 03:18
  • I could see a empty `list` getting added in the result – user007 May 28 '19 at 03:21
  • @user007 yes. See my final comments. You can edit the code yourself to get this to do exactly what you want. I've shown you the general process. – dwjohnston May 28 '19 at 03:22