Creating a simple accordion widget in Ionic 4

January 27, 2019, 9:36 pm Categories:

Categories

Accordions are a popular UI convention for managing the logical grouping and display of related information such as, for example, a staff directory or an FAQs section.

Over the course of this tutorial I'll walk you through creating a simple Ionic 4 accordion component which utilises some key Angular features such as @Input()/@Output() decorators and EventEmitters to manage communication between parent component-child component and child component-parent component.

Here's what we should have created by the end of this tutorial:

Animated demonstration of a custom Ionic/Angular accordion component being interacted with by the user

Ready to begin?

Laying the foundation

Opening up your software terminal (and ensuring you've navigated to a directory location where you would normally store your digital projects) issue the following command to create a blank Ionic project named ionic-accordion:

ionic start ionic-accordion blank --type=angular

Once the project has been generated issue change into the newly created project directory and create a custom Angular component named mi-accordion that sits within a widgets directory with the following commands:

cd ./ionic-accordion
ionic g component widgets/mi-accordion

With the basic skeleton for our project created there is one minor amendment that we need to make to the application root module which involves removing ALL references to the newly generated accordion component (we'll be importing this into the feature module for the Ionic HomePage component instead).

Open the ionic-accordion/src/app/app.module.ts and change the configuration for this file so that all references to the import/declaration for the MiAccordionComponent are removed.

After doing so your application root module should resemble the following:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule }      from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, CommonModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}

With this minor amendment completed and in place let's now turn our attention towards developing the codebase for our recently generated MiAccordionComponent.

The custom component logic

We'll start by focussing on code for the component TypeScript as this features the following Angular modules:

  • @Input() - Defines a component field as a data-bound input property which Angular automatically updates during change detection
  • @Output() - Defines a component property as an output binding (in order to use this we need to import and configure an instance of the EventEmitter module - see next bullet point)
  • EventEmitter - Allows custom events to be synchronously/asynchronously emitted and handlers for those events to be registered by subscribing to the emitted instance

If you're new to the above Angular modules then it might pay to think of them in this way: the @Input() module brings data in to a component and the @Output()/EventEmitter modules allow data to be output/broadcast from a component.

Using @Input()/@Output() and EventEmitter is a standard pattern for communication - in both directions - between parent and child components.

We'll see how this works a little later on in the article.

We begin using these modules in our MiAccordionComponent by importing them like so:

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';

Within the actual MiAccordionComponent class we then define the following properties - some of which we assign our @Input() and @Output() decorators as well as our EventEmitter module to:


/**
   * The name of the technology that will be displayed as the title for the accordion header
   * @public
   * @property name
   * @type {string}
   */
  @Input()
  name : string;


  /**
   * The description of the technology that will be displayed within the accordion body (when activated 
   * by the user)
   * @public
   * @property description
   * @type {string}
   */
  @Input()
  description : string;


  /**
   * The official logo identifying the technology that will be displayed within the accordion body (when activated 
   * by the user)
   * @public
   * @property image
   * @type {string}
   */
  @Input()
  image : string;


  /**
   * The change event that will be broadcast to the parent component when the user interacts with the component's 
   * <ion-button> element
   * @public
   * @property change
   * @type {EventEmitter}
   */
  @Output()
  change : EventEmitter<string> = new EventEmitter<string>();


  /**
   * Determines and stores the accordion state (I.e. opened or closed)
   * @public
   * @property isMenuOpen
   * @type {boolean}
   */
  public isMenuOpen : boolean = false;

Notice how the change property for the @Output() decorator is assigned as a new EventEmitter?

Once again, when using the @Output() decorator, you will need to import and bind an instance of the EventEmitter module to the @output() property in order to broadcast the value you wish the parent component to receive.

Underneath both the class constructor and ngOnInit() default event lifecycle hook we define the following 2 methods:

  • toggleAccordion - Manages the display/non-display of the accordion content
  • broadcastName - Manages the broadcasting of the value for the @Output() decorator change property

Both of which are defined as follows:


  /**
   * Allows the accordion state to be toggled (I.e. opened/closed)
   * @public
   * @method toggleAccordion
   * @returns {none}
   */
  public toggleAccordion(): void {
      this.isMenuOpen = !this.isMenuOpen;
  }


  /**
   * Allows the value for the  element to be broadcast to the parent component
   * @public
   * @method broadcastName
   * @returns {none}
   */
  public broadcastName(name: string): void {
     this.change.emit(name);
  }

Believe it or not that's all the code we require for the MiAccordionComponent class.

In full then the code for the ionic-accordion/src/app/widgets/mi-accordion/mi-accordion.component.ts component is as follows:

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';

@Component({
  selector: 'app-accordion',
  templateUrl: './mi-accordion.component.html',
  styleUrls: ['./mi-accordion.component.scss']
})
export class MiAccordionComponent implements OnInit {


  /**
   * The name of the technology that will be displayed as the title for the accordion header
   * @public
   * @property name
   * @type {string}
   */
  @Input()
  name : string;


  /**
   * The description of the technology that will be displayed within the accordion body (when activated 
   * by the user)
   * @public
   * @property description
   * @type {string}
   */
  @Input()
  description : string;


  /**
   * The official logo identifying the technology that will be displayed within the accordion body (when activated 
   * by the user)
   * @public
   * @property image
   * @type {string}
   */
  @Input()
  image : string;


  /**
   * The change event that will be broadcast to the parent component when the user interacts with the component's 
   * <ion-button> element
   * @public
   * @property change
   * @type {EventEmitter}
   */
  @Output()
  change : EventEmitter<string> = new EventEmitter<string>();


  /**
   * Determines and stores the accordion state (I.e. opened or closed)
   * @public
   * @property isMenuOpen
   * @type {boolean}
   */
  public isMenuOpen : boolean = false;



  constructor() { }



  ngOnInit() {
  }



  /**
   * Allows the accordion state to be toggled (I.e. opened/closed)
   * @public
   * @method toggleAccordion
   * @returns {none}
   */
  public toggleAccordion() : void
  {
      this.isMenuOpen = !this.isMenuOpen;
  }


  /**
   * Allows the value for the <ion-button> element to be broadcast to the parent component
   * @public
   * @method broadcastName
   * @returns {none}
   */
  public broadcastName(name : string) : void
  {
     this.change.emit(name);
  }

}

Relatively simple isn't it? As mentioned above the real 'magic' for the above component will be driven by the @Input()/@Output() decorators and EventEmitter module which allow parent-child communication in both directions.

Let's now move onto the markup structure for our MiAccordionComponent.

The accordion component markup structure

Our component HTML consists of the following structure:

  • <h2> heading - this will display the title for the accordion and contain the toggle button states (which indicate whether the accordion is open or closed)
  • <div> wrapper - this will contain and, when activated, display the 'content' for the accordion

We'll make use of Angular's ngIf directive to manage the display of the toggle button states as well as an ngClass property binding to display/hide the accordion content.

This is actually a pretty simple markup structure which will be placed into the ionic-accordion/src/app/widgets/mi-accordion/mi-accordion.component.html file like so:

<h2 (click)="toggleAccordion()">
  {{ name }}
   <span *ngIf="isMenuOpen">&#9650;</span>
   <span *ngIf="!isMenuOpen">&#9660;</span>
</h2>
<div 
   [ngClass]="this.isMenuOpen ? 'active' : 'inactive'">
   <section class="image-wrapper">
   	  <img [src]="image">
   </section>
   <p>{{ description }}</p>
   <ion-button 
      type="button" 
      color="primary" 
      fill="solid" 
      size="default" 
      (click)="broadcastName(name)">Console log me!</ion-button>
</div>

Notice the <ion-button> element towards the end of the markup?

This will allow the name value for that accordion (which is displayed as a result of the data retrieved from the @Input() field) to be broadcast to the parent component using the broadcastName() method which, as you might remember from the previous section, uses the EventEmitter module to accomplish this.

The necessary styles for the above markup are declared within the accordion component stylesheet - located at ionic-accordion/src/app/widgets/mi-accordion/mi-accordion.component.scss as follows:

h2 {
      cursor: pointer;
      position: relative;
      padding: 1em 0.35em;
      font-size: 1.35em;
      font-family: Verdana;
      border-bottom: 1px solid rgba(210,210,210,1);
      margin: 0;


      /* Define the style rules for the 'arrow icons' */
      span {
         position: absolute;
         right: 1em;
         top: 1.2em;
         font-size: 0.95em;
      }
   }


   /* Here we define the actual 'menu' and its 'options' */
   .image-wrapper {
      margin: 0 auto 2em auto;
      width: 20%;

      img {
         display: block;
         margin: auto;
      }
   }


   p {
      line-height: 1.2em;
      margin: 0 0 1em 0;
      font-family: Verdana;
      font-size: 1rem;
   }


   div {
      position: relative;
      padding: 2em;
      background: rgba(230, 230, 230, 1);
      border-bottom: 1px solid rgba(210, 210, 210, 1);


      ion-button {
         position: absolute;
         bottom: 20px;
         right: 20px;
      }

   }

   /* Following classes display/hide the 'menu'
   // based on the state change detection in the
   // component class */
   .active {
      display: block
   }

   .inactive {
      display: none;
   }

With that we have covered all of the required logic, templating and styling for the MiAccordionComponent so let's now move onto the HomePage component and see how this all fits together.

The home page component logic

Now that we have the foundations for our MiAccordionComponent firmly in place we need to configure our HomePage component to supply the necessary data for each of the @Input() fields within the MiAccordionComponent as well as being able to capture the data broadcast (courtesy of the @Output() decorator and EventEmitter module) from that accordion component.

We begin by defining the data for the MiAccordionComponent which consists of an associative array where each node supplies the following fields:

  • name
  • description
  • image

Notice how these fields exactly match the @Input() properties within our MiAccordionComponent?

Developing from our data source we then, after the HomePage class constructor, define a method named captureName which will simply log the value broadcast by the EventEmitter instance from within the MiAccordionComponent.

In full then, our HomePage class - located at ionic-accordion/src/app/home/home.page.ts - looks like the following:

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {


/**
 * The data structure that will be used for supplying the accordion content
 * @public
 * @property technologies
 * @type {Array}
 */
public technologies : Array<{ name: string, description: string, image: string }> = [
    { 
       name : 'Angular', 
       description : 'Google\'s front-end development framework - default option for Ionic development',
       image: '/assets/images/angular-logo.png'
    },
    { 
       name : 'VueJS', 
       description : 'Latest cutting edge front-end development framework - can be enabled as an option for Ionic development',
       image: '/assets/images/vuejs-logo.png'
    },
    { 
       name : 'React', 
       description : 'Popular front-end development framework from Facebook- can be enabled as an option for Ionic development',
       image: 'assets/images/react-logo.png'
    },
    { 
       name : 'TypeScript', 
       description : 'Superset of JavaScript that provides class based object oriented programming and strict data typing',
       image: 'assets/images/typescript-logo.png'
    },
    { 
       name : 'Ionic Native', 
       description : 'Apache Cordova compatible plugins that allow native device API\'s to be utilised',
       image: 'assets/images/ionic-native-logo.png'
    },
    { 
       name : 'Capacitor', 
       description : 'Plugins for Progressive Web App and hybrid app development',
       image: 'assets/images/capacitor-logo.png'
    },
    { 
       name : 'StencilJS', 
       description : 'Custom web component development framework',
       image: 'assets/images/stencil-logo.png'
    },
    { 
       name : 'Sass', 
       description : 'CSS pre-processor development library',
       image: 'assets/images/sass-logo.png'
    },
    { 
       name : 'HTML5', 
       description : 'Markup language and front-end API support',
       image: 'assets/images/html5-logo.png'
    }
  ];



  constructor() {}


  /**
   * Captures and console logs the value emitted from the user depressing the accordion component's <ion-button> element 
   * @public
   * @method captureName
   * @param {any}		event 				The captured event
   * @returns {none}
   */
  public captureName(event: any) : void
  {
     console.log(`Captured name by event value: ${event}`);
  }

}

With the logic for the HomePage component class in place we now turn our attention towards crafting the associated markup.

The home page component markup

Within the HomePage component template - located at ionic-accordion/src/app/home/home.page.html - we need to place the following code:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Accordion
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  
  <app-accordion 
     *ngFor="let technology of technologies" 
     name="{{ technology.name }}" 
     description="{{ technology.description }}" 
     image="{{ technology.image }}" 
     (change)="captureName($event)"></app-accordion>

</ion-content>

The most important aspects of the above markup are as follows:

  • app-accordion - the actual accordion component tag selector
  • name attribute - our first @Input() field
  • description attribute - our second @Input() field
  • image attribute - our final @Input() field
  • (change) event - this is the @Output() property which calls the captureName method to log the captured data for the accordion

With our markup in place we have one final configuration to perform before we can test our accordion component in the system browser.

The Home page component feature module

Within the HomePage component feature module - located at ionic-accordion/src/app/home/home.module.ts - we import and declare the MiAccordionComponent component like so:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { MiAccordionComponent } from '../widgets/mi-accordion/mi-accordion.component';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ])
  ],
  declarations: [HomePage, MiAccordionComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HomePageModule {}

IMPORTANT: Do NOT forget to import and add the CUSTOM_ELEMENTS_SCHEMA module to the schemas declaration. This allows our custom component to be recognised by Angular when transpiling the project code.

Now we've completed all of the necessary coding and configurations let's test our component!

All systems go

Within the Ionic CLI issue the following command to launch the ionic-accordion project within your system web browser:


ionic serve

All things being well with the code you've entered into each of your component files you should see the project being run in the browser and find yourself able to interact with the accordion like so:

Animated demonstration of a custom Ionic/Angular accordion component being interacted with by the user

In summary

As you have hopefully seen from the above article creating custom components with Ionic/Angular is relatively simple - particularly with use of Angular's @Input()/@Output() decorators and EventEmitter module which facilitate parent-child communication in both directions.

As for the above accordion we created this does the job but it is somewhat limited as we can only close one opened accordion at a time. Implementing the functionality to close multiple opened accordions will be the topic for another article but in the meantime enjoy playing with creating and implementing your own custom components.

If you've enjoyed what you've read and/or found this helpful please feel free to share your comments, thoughts and suggestions in the comments area below.

I explore different aspects of working with the Ionic 4 framework in my e-book featured below and if you're interested in learning more about further articles and e-books that I'm writing please sign up to my FREE mailing list.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top