Angular Accordion

Angular accordion listed under website features is JSON powered with different sectional layouts.

This basic Angular accordion is set up to have multiple layouts. Each of which can be easily configured to apply in any group. The code is super simple as it’s akin to basic JavaScript class toggling. Not really much to it here, but can certainly be used as a component. The number of items on the far right is the amount of objects being piped into each group. Most importantly, this is JSON powered. Acquired via a conventional http GET.

Git Repo

Imported Data

[
  { "name": "Experience",
    "content": [
      { "item": "UI Development"                },
      { "item": "Frontend Development"      },
      { "item": "Single Page Apps"             },
      { "item": "HTML5, CSS, JS & JSON"   },
      { "item": "Data Driven Development"  },
      { "item": "Manage Small Teams"         }
    ]},
  { "name": "Skills",
    "content": [
      { "title": "HTML5"         },
      { "title": "CSS"           },
      { "title": "Sass"          },
      { "title": "JavaScript"    },
      { "title": "TypeScript"    },
      { "title": "Angular"       },
      { "title": "JSON"          },
      { "title": "Spring"        },
      { "title": "Express"       },
      { "title": "Wordpress"     },
      { "title": "Photoshop"     }
    ]},
  { "name": "Codepens",
    "content": [
      { "title": "HTML5 Video Player",
        "code": "https://codepen.io/eastcoastdeveloper/pen/LmxqKa",
        "viewProject": "https://codepen.io/eastcoastdeveloper/full/LmxqKa" },
      { "title": "Progress Navigation",
        "code": "https://codepen.io/eastcoastdeveloper/pen/xXOapm",
        "viewProject": "https://codepen.io/eastcoastdeveloper/full/xXOapm" },
      { "title": "JavaScript Tabs",
        "code": "https://codepen.io/eastcoastdeveloper/pen/yzejjQ",
        "viewProject": "https://codepen.io/eastcoastdeveloper/full/yzejjQ" },
      { "title": "Responsive Carousel",
        "code": "https://codepen.io/eastcoastdeveloper/pen/WyxvbP",
        "viewProject": "https://codepen.io/eastcoastdeveloper/full/WyxvbP" }
    ]
  },
  { "name": "Codepen Projects",
    "content": [
      { "title": "AngularJS SPA",
        "code": "https://codepen.io/eastcoastdeveloper/project/editor/ZgbdLb",
        "viewProject": "https://codepen.io/eastcoastdeveloper/project/full/ZgbdLb" },
      { "title": "Web Banner Template",
        "code": "https://codepen.io/eastcoastdeveloper/project/editor/XYyQaL",
        "viewProject": "https://codepen.io/eastcoastdeveloper/project/full/XYyQaLL" }
    ]
  },
  { "name": "Stackblitz",
    "content": [
      { "title": "Drag & Drop UI",
        "code": "https://stackblitz.com/edit/drag-and-drop-javascript",
        "viewProject": "https://drag-and-drop-javascript.stackblitz.io/" },
      { "title": "Banner Template",
        "code": "https://stackblitz.com/edit/banner-template",
        "viewProject": "https://banner-template.stackblitz.io/" },
      { "title": "Mock UI w/ JSON Charts",
        "code": "https://stackblitz.com/edit/chart-js-examples",
        "viewProject": "https://chart-js-examples.stackblitz.io/" },
      { "title": "Accordion",
        "code": "https://stackblitz.com/edit/angular-accordion-json",
        "viewProject": "https://angular-accordion-json.stackblitz.io/" }
    ]
  },
  { "name": "Posts",
    "content": [
      { "title": "UX Design All Star Tips",
        "link": "https://codepen.io/eastcoastdeveloper/post/ux-design-description-gotchas-all-star-tips" }
    ]}
]

Firstly, we’ll tell TypeScript what to expect by creating an interface with optional values. Values are optional because there’s different layouts. Not all values exist in each layout. The data is comprised of conventional objects nested in an array. This instantly means we’re looping over data sets. There are however nested objects which infer double loops. This is one reason I love Angular. It provides powerful tools to accomplish complex tasks. And as you’ll see below, we’re easily able to maintain indice references.

Angular Accordion Interface

export interface AccordionData {
    content: {
      item?: string;
      title?: string;
      code?: string;
      viewProject?: string;
      link?: string;
    }[];
    name: string;
}

For those of you unfamiliar with this, it’s a representation of the payload. Not possible in JavaScript, but one of the core pieces of TypeScript and abundant in other languages such as Java and frameworks such as Spring and .NET. The type is checked at run time vs compile time. You may see interfaces prefixed with T, such as TAccordionData to help denote a valid type for future developers. Moreover this could certainly be considered a best practice in some environments as an interface or model called T is simply not intuitive enough.

Angular Accordion Component

  • In the TypeScript file, we begin with an empty array set to the interface type.
  • Capture the parent element.
  • OnInit pushes the data into the aforementioned array via an http request.
  • Notice the http reference in the constructor. You’ll also need to add HttpClientModule to the parent module.
  • The subscribe method takes advantage of RxJS’s inbuilt next, error, and complete methods. Of course in a real world scenario, these would be beneficial.
  • The toggle function looks more complex than it really is. The hidden sections are gathered in an array. The line before clears that array. Loop over the elements, and apply the ‘show-content’ class to the appropriate section.
import { HttpClient } from '@angular/common/http';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { AccordionData } from './accordion.interface';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  accordionData: AccordionData[] = [];
  @ViewChild('accordionParent', { static: false }) accordionParent: ElementRef;
  constructor(private _http: HttpClient) {}

  // Get the data, set the interface (obj properties).
  // pipe() holds functions that will be sequentially executed.
  // subscribe w/ next, error, and complete methods
  ngOnInit(): void {
    this._http
      .get<AccordionData[]>('path-to-json')
      .pipe(take(1))
      .subscribe({
        next: (val) => {
          this.accordionData = val;
        },
        error: (err) => {
          console.log(err);
        },
        complete: () => {
          console.log('finished');
        },
      });
  }

  // Toggle Accordion
  toggleSection(e: any, index: number) {
    var allGroups = [];
    allGroups =
      this.accordionParent.nativeElement.getElementsByClassName(
        'hidden-content'
      );
    for (var j = 0; j < allGroups.length; j++) {
      if ( index === j && !e.target.parentElement
          .querySelector('.hidden-content')
          .classList.contains('show-content')
      ) {
        e.target.parentElement
          .querySelector('.hidden-content')
          .classList.add('show-content');
      } else {
        allGroups[j].classList.remove('show-content');
      }
    }
  }
}

Lean Markup

The markup is essentially one giant HTML block with settings for each section. That’s to say, the ngFor directive at the very top. I’ve seen markup get pretty messy and often times have unnecessary tags, parents, and children. Usually written by devs that have no interest in the frontend in my experience. So, regardless of the frontend task, keep the markup as lean as possible. Each section is initialized with a double loop indicative of the payload.

<div #accordionParent class="accordion-wrapper">
  <div class="data-block" *ngFor="let item of accordionData; let i = index">
    <p (click)="toggleSection($event, i)">{{ item.name }}</p>
    <p class="count">{{ item.content.length }}</p>
    <div class="borderBottom"></div>
    <div class="hidden-content">
      <ul *ngIf="i === 0">
        <li *ngFor="let posts of item.content; let j = index">
          <div class="title">{{ posts.item }}</div>
        </li>
      </ul>
      <ul id="skills" *ngIf="i === 1">
        <li *ngFor="let listElems of item.content; let j = index">
          <div class="title">{{ listElems.title }}</div>
        </li>
      </ul>
      <ul *ngIf="i === 2 || i === 3 || i === 4">
        <li *ngFor="let listElems of item.content; let j = index">
          <div class="title">{{ listElems.title }}</div>
          <div class="code-link">
            <a href="{{ listElems.code }}" target="_blank">code</a>
          </div>
          <div class="project-link">
            <a href="{{ listElems.viewProject }}" target="_blank">view project</a>
          </div>
        </li>
      </ul>
      <ul *ngIf="i === 5">
        <li *ngFor="let posts of item.content; let j = index">
          <div class="title">{{ posts.title }}</div>
          <div class="code-link">
            <a href="{{ posts.link }}" target="_blank">Read the post</a>
          </div>
        </li>
      </ul>
    </div>
  </div>
</div>

Apply the Styling

The scss variables at the top are no doubt housed in another file, then imported. Therefore, we set the stage to change the entire color palette in one fell swoop. As mentioned on other pages, I use nesting to compartmentalize styling blocks. Looks cleaner, easier to read, and definitely a safer bet. I must say, even while working on high profile projects—I notice devs writing inline styles. Please, avoid this at all cost! It’s inflexible and definitely frowned upon.

$black: #313b3f;
$blue: #25aae1;
$white: #eff3f6;
$yellow: #d9a74a;

.accordion-wrapper {
  margin: 30px auto 0 auto;
  border-radius: 5px;
  overflow: hidden;
  max-width: 300px;
  .data-block:nth-child(2) {
    border-bottom: 1px solid $white;
  }
}

.data-block {
  font: normal 14px sans-serif;
  cursor: pointer;
  position: relative;
  p {
    background-color: $black;
    color: $yellow;
    margin: 0;
    padding: 10px 0 10px 10px;
  }
  .count {
    position: absolute;
    top: 0;
    right: 15px;
  }
  ul {
    list-style-type: none;
    padding: 0px;
    margin: 0;
    line-height: 25px;
    li {
      display: grid;
      padding: 5px 10px 5px 30px;
      grid-template-areas: 'title title' 'code project';
      .title {
        color: $blue;
        grid-area: title;
      }
      .code-link {
        a { grid-area: code; }
      }
      .project-link {
        a { grid-area: project; }
      }
      div:not(:first-child) { font-style: italic; }
    }
    li:nth-child(even) { background-color: rgba(49, 59, 63, 0.8); }
    li:nth-child(odd)  { background-color: rgba(49, 59, 63, 0.9); }
    li:hover {
      background-color: $black;
    }

    a {
      color: $white;
      text-decoration: none;
      transition: color 0.3s;
    }
    a:hover { color: $yellow; }
  }
}
.hidden-content {
  overflow: auto;
  max-height: 0;
  color: $white;
}
.show-content {
  max-height: 250px !important;
}
#skills {
  list-style-type: unset;
  background-color: $black;
  display: grid;
  grid-template-columns: 50% 50%;
  line-height: 20px;
  padding: 20px 0 10px 30px;
  li {
    display: block;
    padding: 5px 10px 5px 0;
  }
}
.borderBottom {
  border-bottom: 1px solid $white;
  position: absolute;
  top: 36px;
  width: 100%;
  z-index: 1;
}

In Conclusion

Regardless of the project, components like these are always helpful in organizing the UI for any screen size. Making components like this clean, malleable, and easy to maintain are crucial. For example, at my current place of employment, there’s archaic code. Features without documentation and comments. There’s basic components that don’t work or are unresponsive. But I digress. Hope you found this post useful. Check out the Angular Accordion Component on Stackblitz.

Be the first to comment

Leave a Reply

Your email address will not be published.


*