vixalien
vixalien

Reputation: 390

How to flatten a tree of table columns into a flat list of table columns with headers

I have a nested tree of table columns. Here are the typescript types.

export interface TableLeafColumn {
  label: string;
  key?: string;
}

export interface TableContainerColumn {
  label: string;
  columns: TableColumn[];
}

// Union type to represent either a leaf or container column
export type TableColumn = TableLeafColumn<T> | TableContainerColumn;

Here's an example of such a type:

const columns: TableColumn[] = [
  {
    label: "Name",
    key: "name",
  },
  {
    label: "Details",
    columns: [
      {
        label: "Age",
        key: "age",
      },
      {
        label: "Address",
        columns: [
          { label: "City", key: "city" },
          { label: "Country", key: "country" },
        ],
      },
    ],
  },
];

I would like a function called flattenColumns that will return TableColumn[][]. For example, the input above would return:

const result = [
  [
    { label: "" }, // placeholder for Name
    { label: "Details", colSpan: 3 }
  ],
  [
    { label: "" }, // placeholder for Name
    { label: "" }, // placeholder for Age
    { label: "Address", colSpan: 2  }
  ],
  [
    { label: "Name", key: "name" },
    { label: "Age", key: "age" },
    { label: "City", key: "city" },
    { label: "Country", key: "country" },
  ]
]

I have tried to write the function, but nothing worked. The tricky party is adding the placeholders, so that every leaf column appears at the bottom.

Not sure if the result I gave above is the best representation for this, but I want to render such table headers inside an HTML table, and I figured that format would be the easiest to utilise.

Here is my (non-working) attempt.

export function isContainerColumn(
  col: TableColumn,
): col is TableContainerColumn {
  return "columns" in col && Array.isArray(col.columns);
}

interface FlatColumn extends TableLeafColumn {
  colSpan: number;
}

export function flattenColumns(
  columns: TableColumn[],
): FlatColumn[][] {
  const levels: FlatColumn[][] = [];

  function flatten(
    cols: TableColumn<any>[],
    depth: number,
  ): number {
    levels[depth] ??= [];

    let totalSpan = 0;

    cols.forEach((col) => {
      if (isContainerColumn(col)) {
        const childSpan = flatten(col.columns, depth + 1);
        totalSpan += childSpan;

        levels[depth]!.push({
          ...col,
          colSpan: childSpan,
        });
      }
      else {
        levels[depth]!.push({
          ...col,
          colSpan: 1,
        });

        totalSpan += 1;
      }
    });

    return totalSpan;
  }

  flatten(columns, 0);
  return levels;
}

Upvotes: 2

Views: 68

Answers (1)

Nina Scholz
Nina Scholz

Reputation: 386766

You could get colSpan for nested items first and then get rows for each level with prefilling of values.

A more complex example b with

[
    {
        label: "abcdefg",
        columns: [
            { label: "a", key: "a" },
            {
                label: "bcdefg",
                columns: [
                    {
                        label: "bcd",
                        columns: [
                            {
                                label: "bc",
                                columns: [
                                    { label: "b", key: "b" },
                                    { label: "c", key: "c" }
                                ]
                            },
                            { label: "d", key: "d" }
                        ]
                    },
                    { label: "e", key: "e" },
                    {
                        label: "fg",
                        columns: [
                            { label: "f", key: "f" },
                            { label: "g", key: "g" }
                        ]
                    }
                ]
            }
        ]
    },
    {
        label: "hij",
        columns: [
            { label: "h", key: "h" },
            {
                label: "ij",
                columns: [
                    { label: "i", key: "i"}, 
                    { label: "j", key: "j"}
                ]
            }
        ]
    }
]

result:

| abcdefg                   | hij       |
|   | bcdefg                |   | ij    |
|   | bcd       |   | fg    |   |   |   |
|   | bc    |   |   |   |   |   |   |   |
| a | b | c | d | e | f | g | h | i | j |
  1   2   3   4   5   6   7   8   9  10 

const
    convert = columns => {
        const
            sizes = new WeakMap,
            getSizes = (o, wm) => {
                const size = o.columns.reduce((cs, q) => cs + (!q.columns || getSizes(q, wm)), 0) || 1
                wm.set(o, size);
                return size;
            },
            temp = { columns },
            values = [],
            result = [],
            indices = [],
            iterate = (o, level = 0) => {
                result[level] ??= [];
                indices[level] ??= 0;

                while (indices[level] < values.length) {
                    result[level].push({ label: '' });
                    indices[level]++;
                }

                o.columns.forEach(q => {
                    if (q.columns) {
                        const colSpan = sizes.get(q);
                        result[level].push({ label: q.label, colSpan });
                        indices[level] += colSpan;
                        iterate(q, level + 1);
                    } else {
                        values.push(q);
                        result[level].push({ label: '' });
                        indices[level]++;
                    }
                });

            };

        getSizes(temp, sizes);
        iterate(temp);

        result.length--;
        for (let i = 0; i < result.length; i++) {
            while (indices[i] < sizes.get(temp)) {
                result[i].push({ label: '' });
                indices[i]++;
            }
        }

        result.push(values);
        return result;
    },
    a = [{ label: "Name", key: "name" }, { label: "Details", columns: [{ label: "Age", key: "age" }, { label: "Address", columns: [{ label: "City", key: "city" }, { label: "Country", key: "country" }] }] }],
    b = [{ label: "abcdefg", columns: [{ label: "a", key: "a" }, { label: "bcdefg", columns: [{ label: "bcd", columns: [{ label: "bc", columns: [{ label: "b", key: "b" }, { label: "c", key: "c" }] }, { label: "d", key: "d" }] }, { label: "e", key: "e" }, { label: "fg", columns: [{ label: "f", key: "f" }, { label: "g", key: "g" }] }] }] }, { label: "hij", columns: [{ label: "h", key: "h" }, { label: "ij", columns: [{ label: "i", key: "i" }, { label: "j", key: "j" }] }] }];

console.log(convert(a));
console.log(convert(b));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 2

Related Questions