Home > Mobile >  How to transform a specifically structured array of objects into a nested unordered list
How to transform a specifically structured array of objects into a nested unordered list

Time:05-26

I have an array of objects that list out authors and books. They're all in one array, but I could split them out into two arrays (authors and books) if that makes things more straightforward. The current single array is like this:

[{authorId: 1, author: 'Jane Doe', isAuthor: true, type: 'Autobiography'},
{authorId: 2, author: 'John Smith', isAuthor: true, type: 'Fiction'},
{authorId: 3, author: 'Bill Bradley', isAuthor: true, type: 'Non-Fiction'},
{authorId: 4, author: 'Jack Thomas', isAuthor: true, type: 'Autobiography'},
{authorId: 5, author: 'Molly McNeil', isAuthor: true, type: 'Autobiography'},
{authorId: 1, bookId: 50, bookTitle: 'Title 1', isAuthor: false, author: 'Jane Doe', type: 'Autobiography'},
{authorId: 1, bookId: 51, bookTitle: 'Title 2', isAuthor: false, author: 'Jane Doe', type: 'Autobiography'},
{authorId: 2, bookId: 52, bookTitle: 'Title 3', isAuthor: false, author: 'John Smith', type: 'Fiction'},
{authorId: 3, bookId: 53, bookTitle: 'Title 4', isAuthor: false, author: 'Bill Bradley', type: 'Non-Fiction'},
{authorId: 2, bookId: 54, bookTitle: 'Title 5', isAuthor: false, author: 'John Smith', type: 'Fiction'},
{authorId: 4, bookId: 55, bookTitle: 'Title 6', isAuthor: false, author: 'Jack Thomas', type: 'Autobiography'},
{authorId: 4, bookId: 56, bookTitle: 'Title 7', isAuthor: false, author: 'Jack Thomas', type: 'Autobiography'}]

The actual array of objects can be several thousand so I need to keep performance in mind.

I need to display everything in a specific way and I can't seem to get this to work. Each author should have its own LI with a UL within that lists each book. However, that entire block should be under a parent LI with the "type". Something like this:

<ul id="myList">
   <li >
      <span >Autobiography</span>
      <ul>
         <li>
            <span >Jane Doe</span>
            <ul>
               <li><span >Title 1</span></li>
               <li><span >Title 2</span></li>
            </ul>
         </li>
      </ul>
   </li>
   <li >
      <span >Autobiography</span>
      <ul>
         <li>
            <span >Jack Thomas</span>
            <ul>
               <li><span >Title 6</span></li>
               <li><span >Title 7</span></li>
            </ul>
         </li>
      </ul>
   </li>
   <li >
      <span >Autobiography</span>
      <ul>
         <li>
            <span >Molly McNeil</span>
         </li>
      </ul>
   </li>
   <li >
      <span >Fiction</span>
      <ul>
         <li>
            <span >John Smith</span>
            <ul>
               <li><span >Title 3</span></li>
               <li><span >Title 5</span></li>
            </ul>
         </li>
      </ul>
   </li>
   <li >
      <span >Non-Fiction</span>
      <ul>
         <li>
            <span >Bill Bradley</span>
            <ul>
               <li><span >Title 4</span></li>
            </ul>
         </li>
      </ul>
   </li>
</ul>

I think I should try to transfor my array of objects first before display them in the DOM, but I can't seem to get things quite right. Struggling to find a solution that keeps performance in mind. Any help is appreciated. Thanks!

EDIT: there's also a use case where an author may not have any books, but I'd still like them listed in the list. Array and HTML updated above

CodePudding user response:

Presented below is one possible way to achieve the desired objective.

Code Snippet

// method to generate the desired html
const generateHtml = myData => {
  // first get all unique types
  const allTypes = [
    ...new Set(myData.map(({ type }) => type))
  ];
  
  // next, prepare a dictionary to map types & authors
  const typeAuthorMap = Object.fromEntries(
    allTypes.map(
      typ => ([
        typ,
        myData.filter(({ type, isAuthor }) => type === typ && isAuthor)
      ])
    )
  );

  // initialize our result html as an object
  const myDiv = {innerHTML : ''};

  // populate the first ul
  myDiv.innerHTML = `<ul id="myList">`;
  
  // iterate over types, then authors, and then books
  // at each level update the result-html
  allTypes.forEach(typ => {
    myDiv.innerHTML  = `<li ><span >${typ}</span><ul>`;
    typeAuthorMap[typ].forEach(auth => {
      myDiv.innerHTML  = `<li><span >${auth.author}</span><ul>`
      myData.filter(
        ({ author, type, bookTitle }) => (
          bookTitle && author === auth.author && type === typ
        )
      ).forEach(book => {
        myDiv.innerHTML  = `<li><span >${book.bookTitle}</span></li>`
      });
      myDiv.innerHTML  = `</ul></li>`
    });
    myDiv.innerHTML  = `</ul></li>`;
  });
  
  // return the result html
  return myDiv.innerHTML;
};

const myData = [{authorId: 1, author: 'Jane Doe', isAuthor: true, type: 'Autobiography'},
{authorId: 2, author: 'John Smith', isAuthor: true, type: 'Fiction'},
{authorId: 3, author: 'Bill Bradley', isAuthor: true, type: 'Non-Fiction'},
{authorId: 4, author: 'Jack Thomas', isAuthor: true, type: 'Autobiography'},
{authorId: 1, bookId: 50, bookTitle: 'Title 1', isAuthor: false, author: 'Jane Doe', type: 'Autobiography'},
{authorId: 1, bookId: 51, bookTitle: 'Title 2', isAuthor: false, author: 'Jane Doe', type: 'Autobiography'},
{authorId: 2, bookId: 52, bookTitle: 'Title 3', isAuthor: false, author: 'John Smith', type: 'Fiction'},
{authorId: 3, bookId: 53, bookTitle: 'Title 4', isAuthor: false, author: 'Bill Bradley', type: 'Non-Fiction'},
{authorId: 2, bookId: 54, bookTitle: 'Title 5', isAuthor: false, author: 'John Smith', type: 'Fiction'},
{authorId: 4, bookId: 55, bookTitle: 'Title 6', isAuthor: false, author: 'Jack Thomas', type: 'Autobiography'},
{authorId: 4, bookId: 56, bookTitle: 'Title 7', isAuthor: false, author: 'Jack Thomas', type: 'Autobiography'}];

// set number of rows to simulate for types, authors and books
const simulateTypes = 10, simulateAuthors = 250, simulateBooks = 750;

// simulate the additional types
const simulatedTypes = [...Array(simulateTypes).keys()].map(x => `Type${ x 1}`);

// simulate additional authors
const simulatedAuthors = [
  ...Array(simulateAuthors).keys()
]
.map(x =>  x   100)
.map(authorId => ({
  authorId,
  author: `Author Name ${authorId}`,
  isAuthor: true,
  type: simulatedTypes[
    Math.floor(
      Math.random() * simulatedTypes.length
    )
  ]
}));

// simulate additional books
const simulatedBooks = [
  ...Array(simulateBooks).keys()
]
.map(x =>  x  1000)
.map(bookId => ({
  bookId,
  bookTitle: `Book Title ${bookId}`,
  ...simulatedAuthors[
    Math.floor(
      Math.random() * simulatedAuthors.length
    )
  ],
  isAuthor: false,
}))
;

// target the div with id "rd" 
const myDivRender = document.getElementById("rd");
// invoke generateHtml with our data
// and store result to div either as raw
let generatedHtml = generateHtml(myData);
myDivRender.innerHTML = generatedHtml;

// allow user to switch between viewing
// raw html or rendered html
document.getElementById("btn").addEventListener('click', function() {
  myDivRender.innerHTML = '';
  myDivRender.innerText = '';
  if (this.textContent.includes("Raw")) {
    this.textContent = "View rendered result";
    myDivRender.innerText = generatedHtml;
  } else {
    this.textContent = "View Raw HTML";
    myDivRender.innerHTML = generatedHtml;
  }
});

// allow user to simulate 1000's of rows
// of data to validate performance
document.getElementById("simu").addEventListener('click', function() {
  myDivRender.innerHTML = '';
  if (this.textContent.includes("1000")) {
    this.textContent = "Switch off simulation";
    // invoke the generateHtml with concatenation
    // of myData, and additional authors, books
    generatedHtml = generateHtml([
      ...myData,
      ...simulatedAuthors,
      ...simulatedBooks
    ]);
    // store the result as rendered-html
    myDivRender.innerHTML = generatedHtml;
    // update the button text for viewing back in raw html
    document.getElementById("btn").textContent = "View Raw HTML";
  } else {
    this.textContent = "Simulate 1000s of rows";
    // regenerate data without simulated additional rows
    generatedHtml = generateHtml(myData);
    // render it as html
    myDivRender.innerHTML = generatedHtml;
    // update the button text for viewing raw html
    document.getElementById("btn").textContent = "View Raw HTML";
  }
});
.parent {
  color: blue;
};

.header {
  
};

.author {
  
}

.bookTitle {
  color: black;
  text-decoration: underline;
};
<div id="test"></div>
Demo using stack snippets
<hr/>
<button id="simu">Simulate 1000s of rows</button>
<button id="btn">View Raw HTML</button>
<hr/>
<div id="rd"></div>

Explanation

Inline comments added to the snippet above.

EDIT

Simulated 1000 rows of additional data to demo performance.

actual array of objects can be several thousand so I need to keep performance in mind

Disclaimer / Note

In case of massive data, it may be prudent to consider pagination when rendering on UI. Users rarely require the 100th or 1000th row on the screen. One may also consider using some sort of a table, along with multiple filter & search options that will avoid having to render 1000's of items as list & unordered-list.

CodePudding user response:

Can't speak to the performance but here is an example of getting the output you want:

let list = [
  { authorId: 1, author: 'Jane Doe', isAuthor: true, type: 'Autobiography' },
  { authorId: 2, author: 'John Smith', isAuthor: true, type: 'Fiction' },
  { authorId: 3, author: 'Bill Bradley', isAuthor: true, type: 'Non-Fiction' },
  { authorId: 4, author: 'Jack Thomas', isAuthor: true, type: 'Autobiography' },
  { authorId: 5, author: 'Tom Do', isAuthor: true, type: 'Autobiography' }, // Added for demo
  { authorId: 6, author: 'Zack Crow', isAuthor: true, type: 'Fiction' }, // Added for demo
  { authorId: 7, author: 'Mary Who', isAuthor: true, type: 'Comedy' }, // Added for demo
  { authorId: 1, bookId: 50, bookTitle: 'Title 1', isAuthor: false, author: 'Jane Doe', type: 'Autobiography' },
  { authorId: 1, bookId: 51, bookTitle: 'Title 2', isAuthor: false, author: 'Jane Doe', type: 'Autobiography' },
  { authorId: 2, bookId: 52, bookTitle: 'Title 3', isAuthor: false, author: 'John Smith', type: 'Fiction' },
  { authorId: 3, bookId: 53, bookTitle: 'Title 4', isAuthor: false, author: 'Bill Bradley', type: 'Non-Fiction' },
  { authorId: 2, bookId: 54, bookTitle: 'Title 5', isAuthor: false, author: 'John Smith', type: 'Fiction' },
  { authorId: 4, bookId: 55, bookTitle: 'Title 6', isAuthor: false, author: 'Jack Thomas', type: 'Autobiography' },
  { authorId: 4, bookId: 56, bookTitle: 'Title 7', isAuthor: false, author: 'Jack Thomas', type: 'Autobiography' }
]

let allAuthors = list.filter((i) => i.isAuthor) // Array of all authors
let allBooks = list.filter((i) => !i.isAuthor) // Array of all books
let allTypes = new Set(...[allAuthors.map((a) => a.type).sort()]) // Set of types sorted

let noBooks = allAuthors.filter((i) => !allBooks.find((b) => b.author === i.author)) // Array of all authors with no books 

let template = ``

allTypes.forEach((type) => {
  // Books under each type
  let books = allBooks.filter((b) => b.type === type)
  // Books grouped by each author
  let authors = books.reduce((obj, book) => {
    return {
      ...obj,
      [book['author']]: (obj[book['author']] || []).concat(book)
    }
  }, {})

  // Add authors with no books
  noBooks
    .filter((nb) => nb.type === type)
    .forEach((nb) => {
      authors[nb.author] = [{ type: nb.type, author: nb.author }]
    })

  // Construct template
  Object.keys(authors).forEach((author) => {
    template  = `
       <li >
           <span >${authors[author][0].type}</span>
             <ul><li><span >${authors[author][0].author}</span><ul>
       `
    // List each book for the author
    authors[author].forEach((book) => {
      if (book.bookTitle) {
        template  = `<li><span >${book.bookTitle}</span></li>`
      }
    })

    template  = `</ul></li></ul></li>`
  })
})

document.getElementById('myList').innerHTML = template
<ul id="myList"></ul>

Edit

Per OP's comment, added scenario where an author may not have any books but still should be on the output.

  • Related