LIDOR SYSTEMS

Advanced User Interface Controls and Components

How to Create Custom Header for Angular Accordion

Created: 10 May 2017

In Accordion, group header usually is consisted on text in one line. In order to add more elements to the header, you need to create a custom template that will replace the default appearance. In this article, you will learn how to create an accordion header with icon, title in two lines and action buttons on the right side.

Accordion component is part of IntegralUI Web
a suite of UI Components for development of web apps

If you have any questions, don't hesitate to contact us at support@lidorsystems.com

Above presentation shows an Accordion where group header has an icon, text and two buttons: edit and delete. Whenever the edit button is clicked, a text editor will appear where you can enter a new title for the group. As for the delete button, when clicked it will remove the group from the list.

In addition, all groups appear in blue color theme, which overrides the default gray theme.

How to Create Custom Header for Accordion Groups

In order to create a custom header, you need to modify the template of the group header. In this example, the header will have an icon on the left, followed by a title and subtitle, and action buttons aligned on the right side.

To get access to the template for the group header, use the iui-group-header tag. The content of this element is transcluded and placed in the group header space

In case of an icon, you can use an image or the element, which as background has an icon. For group title, we have text in two lines. In order for text lines to appear correctly aligned after the icon, you need to put them in a inline block, like this:

// 
// app.template.html
//

<iui-accordion [groups]="data" #accordion>
   <iui-groupbox *ngFor="let group of data" [controlStyle]="groupStyle">
        <iui-group-header>
            <div class="custom-group-header">
                <span class="icons {{group.icon}}"></span>
                <div class="acc-title-block">
                    <input *ngIf="group==editGroup" type="text" [(ngModel)]="group.title" (keydown)="editorKeyDown($event)" [iuiFocus]="editorFocused" (focus)="selectContent($event)" (blur)="editorLostFocus()" />
                    <span *ngIf="group!=editGroup" class="acc-group-title">{{group.title}}</span><br/>
                    <span *ngIf="group!=editGroup" class="acc-group-subtitle">{{group.subtitle}}</span>
                </div>
                <div class="acc-command-buttons" (mousedown)="onMouseDownCommandButtons($event)">
                    <div (click)="showEditor(group)"><span class="acc-cmd-btn acc-btn-edit"></span></div>
                    <div (click)="deleteGroup(group)"><span class="acc-cmd-btn acc-btn-delete"></span></div>
                </div>
            </div>
        </iui-group-header>
        <div class="acc-cuh-group-content">{{group.body}}</div>
    </iui-groupbox>
</iui-accordion>
                            
/*
* sample-styles.css
*/ 

.custom-group-header
{
    display: inline-block;
    padding: 7px 0;
}
.acc-title-block
{
    margin-left: 5px;
    display: inline-block;
    vertical-align: middle;
}
.acc-group-title
{
}
.acc-group-subtitle
{
    font-size: 0.875em;
    font-style: italic;
    opacity: 0.6;
}
.acc-cuh-icons
{
    background: url(app/integralui/resources/icons-x24.png) no-repeat 0 0;
    display: inline-block;
    padding: 0 !important;
    margin: 0 1px 0 5px;
    width: 24px;
    height: 24px;
    vertical-align: middle;
}
.economics
{
    background-position: -24px -72px;
}
.heart
{
    background-position: -168px -72px;
}
.sports
{
    background-position: -96px -72px;
}
                            

Add Command Buttons to Accordion Header

It is useful to have some command buttons in accordion header. In this example, we have an edit and delete buttons.

These buttons are represented by inline block elements, aligned and placed on the right side of the group header. If you move the mouse cursor over them, their background will change showing a border. This makes it clear which button is currently in action.

// 
// app.component.ts
//

onMouseDownCommandButtons(e: any){
    e.stopPropagation();
}

deleteGroup(group: any){
    this.accordion.removeGroup(group);
}
                            
// 
// app.template.html
//

<iui-accordion [groups]="data" #accordion>
   <iui-groupbox *ngFor="let group of data" [controlStyle]="groupStyle">
        <iui-group-header>
            <div class="custom-group-header">
                <span class="icons {{group.icon}}"></span>
                <div class="acc-title-block">
                    <input *ngIf="group==editGroup" type="text" [(ngModel)]="group.title" (keydown)="editorKeyDown($event)" [iuiFocus]="editorFocused" (focus)="selectContent($event)" (blur)="editorLostFocus()" />
                    <span *ngIf="group!=editGroup" class="acc-group-title">{{group.title}}</span><br/>
                    <span *ngIf="group!=editGroup" class="acc-group-subtitle">{{group.subtitle}}</span>
                </div>
                <div class="acc-command-buttons" (mousedown)="onMouseDownCommandButtons($event)">
                    <div (click)="showEditor(group)"><span class="acc-cmd-btn acc-btn-edit"></span></div>
                    <div (click)="deleteGroup(group)"><span class="acc-cmd-btn acc-btn-delete"></span></div>
                </div>
            </div>
        </iui-group-header>
        <div class="acc-cuh-group-content">{{group.body}}</div>
    </iui-groupbox>
</iui-accordion>
                            
/*
* sample-styles.css
*/ 

.acc-command-buttons
{
    display: inline-block;
    position: absolute;
    right: 5px;
    top: 7px;
    padding-left: 5px;
}
.acc-command-buttons > div
{
    border: thin solid transparent;
    display: inline-block;
    opacity: 0.5;
    padding: 2px;
}
.acc-command-buttons > div:hover
{
    border-color: gray;
    background: white;
    opacity: 1;
}
.acc-cmd-btn
{
    background-image: url(../app/integralui/resources/icons.png);
    background-repeat: no-repeat;
    display: inline-block;
    overflow: hidden;
    padding: 0;
    margin: 0;
    width: 16px;
    height: 16px;
    float: right;
}
.acc-btn-edit
{
    background-position: -128px -81px;
}
.acc-btn-delete
{
    background-position: -160px -96px;
}
                            

When delete button is clicked, related group is removed from the accordion. This is handled by adding a click event to the button and use of removeGroup method from the accordion component.

As for functionality of the edit button, read below.

How to Edit the Accordion Group Title

In order to edit the group title, you can create a text editor that will appear when edit button is clicked. In this example, we are using the HTML input element, which by default is hidden.

When you click on edit button from group header, the group title is replaced by the text editor. This is handled by a variable called editGroup, which determines which group is in edit mode, if any.

When text editor appears, you can enter a new title for the group and confirm the change by pressing the ENTER key. If you press the ESCAPE key or click anywhere outside the editor, the change is cancelled and the editor is closed.

This is all done by handling the keyDown, focus and blur events from the input element. In addition, we are using the iuiFocus directive to move the keyboard focus to the input element, when it appears in the group header. When the input element has the focus, make sure the whole text is highlighted. You can do this, by calling the selectContent method as event handler for the focus event.

// 
// app.component.ts
//

showEditor(group: any){
    this.originalText = group.title;
    this.isEditActive = true;
    this.editGroup = group;
    this.editorFocused = true;
}

// Selects the whole text in the text editor
selectContent(e: any){
    if (e.target){
        setTimeout(function(){
            e.target.setSelectionRange(0, e.target.value.length);
        }, 1);
    }
}

closeEditor(){
    this.editGroup = null;
    this.originalText = '';
    this.editorFocused = false;
}

editorKeyDown(e: any){
    if (this.editGroup){
        switch (e.keyCode){
            case 13: // ENTER
                this.closeEditor();
                break;
                
            case 27: // ESCAPE
                this.editGroup.title = this.originalText;
                this.closeEditor();
                break;
        }
    }
}

editorLostFocus(){
    if (this.editGroup)
        this.editGroup.title = this.originalText;

    this.closeEditor();
}
                            
// 
// app.template.html
//

<iui-accordion [groups]="data" #accordion>
   <iui-groupbox *ngFor="let group of data" [controlStyle]="groupStyle">
        <iui-group-header>
            <div class="custom-group-header">
                <span class="icons {{group.icon}}"></span>
                <div class="acc-title-block">
                    <input *ngIf="group==editGroup" type="text" [(ngModel)]="group.title" (keydown)="editorKeyDown($event)" [iuiFocus]="editorFocused" (focus)="selectContent($event)" (blur)="editorLostFocus()" />
                    <span *ngIf="group!=editGroup" class="acc-group-title">{{group.title}}</span><br/>
                    <span *ngIf="group!=editGroup" class="acc-group-subtitle">{{group.subtitle}}</span>
                </div>
                <div class="acc-command-buttons" (mousedown)="onMouseDownCommandButtons($event)">
                    <div (click)="showEditor(group)"><span class="acc-cmd-btn acc-btn-edit"></span></div>
                    <div (click)="deleteGroup(group)"><span class="acc-cmd-btn acc-btn-delete"></span></div>
                </div>
            </div>
        </iui-group-header>
        <div class="acc-cuh-group-content">{{group.body}}</div>
    </iui-groupbox>
</iui-accordion>
                            

Putting all Together

The code for whole sample is available here:

// 
// app.module.ts
//

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { IntegralUIModule } from './integralui/integralui.module';

import { AppComponent }   from './app.component';

@NgModule({
    imports:      [ 
          BrowserModule, 
          FormsModule, 
          IntegralUIModule
    ],
    declarations: [ 
        AppComponent, 
    ],
    bootstrap: [ AppComponent ]
})
export class AppModule { }

// 
// app.component.ts
//

import { Component, ViewContainerRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { IntegralUIAccordion } from '../../integralui/components/integralui.accordion';

@Component({
    selector: 'iui-app',
    templateUrl: 'app.template.html',
    styleUrls: ['sample-styles.css'],
    encapsulation: ViewEncapsulation.None
})
export class AppComponent {
    @ViewChild('accordion') accordion: IntegralUIAccordion;

    public data: Array = [];

    private isEditActive: boolean = false;
    private editGroup: any = null;
    private originalText: string = '';
    private editorFocused: boolean = false; 

    public groupStyle: any = {
        header: {
            general: {
                normal: 'acc-cuh-group-header',
                hovered: 'acc-cuh-group-header-hovered',
                selected: 'acc-cuh-group-header-selected'
            }
        }
    }

    constructor(){
        this.data = [
            { 
                id: 1,
                icon: 'acc-cuh-icons economics',
                title: 'Economics',
                subtitle: 'Contains a list of topics about economics',
                body: 'Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat.'
            },
            { 
                id: 2,
                icon: 'acc-cuh-icons heart',
                title: 'Health',
                subtitle: 'Contains a list of topics about medicine',
                body: 'Fusce convallis, mauris imperdiet gravida bibendum, nisl turpis suscipit mauris, sed placerat ipsum urna sed risus. In convallis tellus a mauris. Curabitur non elit ut libero tristique sodales. Mauris a lacus. Donec mattis semper leo. In hac habitasse platea dictumst. Fusce convallis, mauris imperdiet gravida bibendum, nisl turpis suscipit mauris, sed placerat ipsum urna sed risus. In convallis tellus a mauris. Curabitur non elit ut libero tristique sodales. Mauris a lacus. Donec mattis semper leo. In hac habitasse platea dictumst.'
            },
            { 
                id: 3,
                icon: 'acc-cuh-icons sports',
                title: 'Sport',
                subtitle: 'Contains a list of topics about different sports',
                body: 'Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor.'
            }
        ];
    }  

    onMouseDownCommandButtons(e: any){
        e.stopPropagation();
    }

    showEditor(group: any){
        this.originalText = group.title;
        this.isEditActive = true;
        this.editGroup = group;
        this.editorFocused = true;
    }

    // Selects the whole text in the text editor
    selectContent(e: any){
        if (e.target){
            setTimeout(function(){
                e.target.setSelectionRange(0, e.target.value.length);
            }, 1);
        }
    }

    closeEditor(){
        this.editGroup = null;
        this.originalText = '';
        this.editorFocused = false;
    }

    editorKeyDown(e: any){
        if (this.editGroup){
            switch (e.keyCode){
                case 13: // ENTER
                    this.closeEditor();
                    break;
                    
                case 27: // ESCAPE
                    this.editGroup.title = this.originalText;
                    this.closeEditor();
                    break;
            }
        }
    }

    editorLostFocus(){
        if (this.editGroup)
            this.editGroup.title = this.originalText;

        this.closeEditor();
    }

    deleteGroup(group: any){
        this.accordion.removeGroup(group);
    }
}
                            
// 
// app.template.html
//

<iui-accordion [groups]="data" #accordion>
   <iui-groupbox *ngFor="let group of data" [controlStyle]="groupStyle">
        <iui-group-header>
            <div class="custom-group-header">
                <span class="icons {{group.icon}}"></span>
                <div class="acc-title-block">
                    <input *ngIf="group==editGroup" type="text" [(ngModel)]="group.title" (keydown)="editorKeyDown($event)" [iuiFocus]="editorFocused" (focus)="selectContent($event)" (blur)="editorLostFocus()" />
                    <span *ngIf="group!=editGroup" class="acc-group-title">{{group.title}}</span><br/>
                    <span *ngIf="group!=editGroup" class="acc-group-subtitle">{{group.subtitle}}</span>
                </div>
                <div class="acc-command-buttons" (mousedown)="onMouseDownCommandButtons($event)">
                    <div (click)="showEditor(group)"><span class="acc-cmd-btn acc-btn-edit"></span></div>
                    <div (click)="deleteGroup(group)"><span class="acc-cmd-btn acc-btn-delete"></span></div>
                </div>
            </div>
        </iui-group-header>
        <div class="acc-cuh-group-content">{{group.body}}</div>
    </iui-groupbox>
</iui-accordion>
                            
/*
* sample-styles.css
*/ 

/* General Settings */
.iui-accordion
{
    float: left;
    width: 400px;
}
.acc-cuh-group-header
{
    background-color: #6791e1;
    border: solid thin #5f8bde;
}
.acc-cuh-group-header-hovered
{
    background-color: #83a6e7;
}
.acc-cuh-group-header-selected
{
    background-color: #2455b0;
    border-color: #1e4691;
    color: white;
}
.acc-cuh-group-content
{
    padding: 50px 0;
    border: thin solid #bbbbbb;
    padding: 10px;
    border-top-color: transparent;
}
.iui-header-expand-box
{
    top: 2px;
    right: 2px;
}
.custom-group-header
{
    display: inline-block;
    padding: 7px 0;
}

/* Header Title */
.acc-title-block
{
    margin-left: 5px;
    display: inline-block;
    vertical-align: middle;
}
.acc-group-title
{
}
.acc-group-subtitle
{
    font-size: 0.875em;
    font-style: italic;
    opacity: 0.6;
}
.acc-cuh-icons
{
    background: url(app/integralui/resources/icons-x24.png) no-repeat 0 0;
    display: inline-block;
    padding: 0 !important;
    margin: 0 1px 0 5px;
    width: 24px;
    height: 24px;
    vertical-align: middle;
}
.economics
{
    background-position: -24px -72px;
}
.heart
{
    background-position: -168px -72px;
}
.sports
{
    background-position: -96px -72px;
}

/* Command Buttons */
.acc-command-buttons
{
    display: inline-block;
    position: absolute;
    right: 5px;
    top: 7px;
    padding-left: 5px;
}
.acc-command-buttons > div
{
    border: thin solid transparent;
    display: inline-block;
    opacity: 0.5;
    padding: 2px;
}
.acc-command-buttons > div:hover
{
    border-color: gray;
    background: white;
    opacity: 1;
}
.acc-cmd-btn
{
    background-image: url(../app/integralui/resources/icons.png);
    background-repeat: no-repeat;
    display: inline-block;
    overflow: hidden;
    padding: 0;
    margin: 0;
    width: 16px;
    height: 16px;
    float: right;
}
.acc-btn-edit
{
    background-position: -128px -81px;
}
.acc-btn-delete
{
    background-position: -160px -96px;
}
                            

Conclusion

By creating a custom template for group header of Accordion component, you can add custom HTML elements or other Angular components and replace the default appearance of the accordion. Furthermore, adding command buttons in header makes it more user friendly with functionality that allows editing or deleting groups on demand.

The Accordion component is part of IntegralUI Web.

Did you Like this Article?


Enter your e-mail address below and you will receive latest articles as well as news on upcoming events and special offers.