Enhancing User Interactions: Building a Selectable and Searchable Data List with Random User API and Input-Output Decorators (Part 21) in Your Angular-15 Ionic-7 App

Welcome back to our ongoing series on building a multiplatform application with Angular-15 and Ionic-7! In our previous articles, we explored a wide range of topics, including authentication, data management, task organization, calendar integration, event management, media management, UI customization, enhanced user authentication, and real-time data manipulation. Today, in Part 21, we're going to dive into creating a selectable and searchable data list using the Random User API and Angular's Input and Output decorators.

Data lists are a common component in many applications, providing users with a convenient way to browse and interact with data. By leveraging the Random User API, we can generate a dataset of random user information to populate our list. Additionally, by utilizing Angular's Input and Output decorators, we can enable data filtering and selection functionality, enhancing the usability and interactivity of our app.

In this installment, we'll guide you through the process of creating a selectable and searchable data list in our Angular-15 Ionic-7 app. We'll start by integrating the Random User API and retrieving a dataset of random user information.


Next, we'll explore the implementation of Angular's Input and Output decorators to enable data filtering and selection. We'll demonstrate how to pass data to the component, filter the data based on user input, and emit selected items to other parts of our app.

Throughout this tutorial, we'll emphasize best practices for component design, data handling, and user interaction. By the end of this article, you'll have a solid understanding of how to create a selectable and searchable data list using the Random User API and Angular's Input and Output decorators, enhancing the interactivity and functionality of your Angular-15 Ionic-7 app.

So, join us in Part 21 of our series as we dive into the creation of a selectable and searchable data list. Together, let's harness the power of the Random User API and Angular's Input and Output decorators to build a dynamic and user-friendly data browsing experience within our application.

Tutorial

Here I have built a feature that allows the user to search through a list of students, select some of them and view their details. First I will create a component that will act as the search bar with the search items via ionic generate component components/searchable-select and a page where this search bar will open up from via ionic generate page pages/search-students. Now I’ll write the logic to search-students.page.ts file in the following way:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { SearchableSelectComponent } from 'src/app/components/searchable-select/searchable-select.component';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-search-students',
  templateUrl: './search-students.page.html',
  styleUrls: ['./search-students.page.scss'],
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule, SearchableSelectComponent],
})
export class SearchStudentsPage implements OnInit {
  users = [];
  selectedUsers: any[] = [];
  constructor(private http: HttpClient) {
    this.loadUsers();
  }

  //https://jsonplaceholder.typicode.com/users

  ngOnInit() {}
  loadUsers() {
    this.http
      .get<any>('<https://randomuser.me/api/?results=30&seed=karan>') // getting users from random user api where seed allows us to get the same data every time
      .subscribe((data: any) => {
        // console.log(data);
        this.users = data.results;
        console.log(this.users);
      });
  }

  selectChanged(event: any) {
    console.log(event);
    this.selectedUsers = event;
  }
}

The above code displays a list of users obtained from the "https://randomuser.me" API. The SearchStudentsPage class implements the OnInit lifecycle hook and has a property named users that is an empty array. The selectedUsers property is an array that holds the users selected by the user.

In the constructor, an instance of the HttpClient is injected to make an HTTP GET request to the randomuser API to fetch 30 users with the same seed every time i.e. the same users. The loadUsers() method sends the GET request and subscribes to the observable to receive the data. The response is stored in the users property.

The selectChanged() method is called whenever a user is selected or deselected. It logs the event to the console and updates the selectedUsers property with the selected user.

Next I will write the template for the above logic in search-students.page.html in the following way:

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>Search Students</ion-title>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
    <ion-buttons slot="end">
      <ion-menu-button menu="main-menu"></ion-menu-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Search Students</ion-title>
    </ion-toolbar>
  </ion-header>
  <ion-list>
    <ion-item
      color="primary"
      lines="full"
      class="ion-margin-vertical"
      detail="true"
      (click)="select.isOpen = true"
    >
      <ion-icon slot="start" name="search"></ion-icon>
      <ion-label>Search Students</ion-label>
    </ion-item>

    <app-searchable-select
      title="All Students"
      [data]="users"
      itemTextField="name.first"
      #select
      [multiple]="true"
      (selectChanged)="selectChanged($event)"
    ></app-searchable-select>
  </ion-list>

  <ion-accordion-group class="ion-margin-top" *ngIf="selectedUsers.length">
    <ion-accordion *ngFor="let user of selectedUsers">
      <ion-item slot="header">
        <ion-avatar class="ion-margin-end">
          <img title="avatar" [src]="user.picture.thumbnail" />
        </ion-avatar>
        <ion-label>{{user.name.first}}</ion-label>
      </ion-item>
      <div class="ion-padding" slot="content">
        <ion-list lines="none">
          <ion-item>
            <ion-label
              >Full Name: {{user.name.title}} {{user.name.first}}
              {{user.name.last}}</ion-label
            >
          </ion-item>
          <ion-item>
            <ion-label>Email: {{user.email}}</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Cell: {{user.cell}}</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Phone: {{user.phone}}</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Gender: {{user.gender}}</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>DOB: {{user.dob.date | date }}</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Age: {{user.dob.age}}</ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Nationality: {{user.nat}}</ion-label>
          </ion-item>
          <ion-item>
            <ion-label
              >Street: {{user.location.street.number}},
              {{user.location.street.name}}</ion-label
            >
          </ion-item>
          <ion-item>
            <ion-label
              >City: {{user.location.city}}, {{user.location.state}}</ion-label
            >
          </ion-item>
          <ion-item>
            <ion-label>Country: {{user.location.country}} </ion-label>
          </ion-item>
          <ion-item>
            <ion-label>Postcode: {{user.location.postcode}}</ion-label>
          </ion-item>
          <ion-item>
            <ion-img [src]="user.picture.large"></ion-img>
          </ion-item>
        </ion-list>
      </div>
    </ion-accordion>
  </ion-accordion-group>
</ion-content>

The ion-content element takes up the full screen and contains an ion-list with an ion-item that displays the "Search Students" label and an ion-icon for searching. Clicking on this item opens an app-searchable-select component, which is a custom component that allows users to search and select items from a list. The "app-searchable-select" component is populated with data from the users array.

When the user selects one or more items from the app-searchable-select component, an ion-accordion-group is displayed below the search bar. This group contains ion-accordion elements, one for each selected user. Each ion-accordion element displays details about the selected user, including their name, email, cell, phone, gender, date of birth, age, nationality, address, and profile picture.

The selectChanged() method is called when the user selects one or more items from the "app-searchable-select" component. This method updates the selectedUsers array with the selected items.

In parallel, I have written the logic to make the search component functional in searchable-select.component.ts in the following way:

import { CommonModule } from '@angular/common';
import {
  Component,
  EventEmitter,
  Input,
  OnChanges,
  OnInit,
  Output,
  SimpleChanges,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IonicModule, SearchbarCustomEvent } from '@ionic/angular';
import { SearchStudentsPage } from 'src/app/pages/search-students/search-students.page';

@Component({
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule, SearchStudentsPage],
  selector: 'app-searchable-select',
  templateUrl: './searchable-select.component.html',
  styleUrls: ['./searchable-select.component.scss'],
})
export class SearchableSelectComponent implements OnChanges {
  @Input() title = 'Search'; //input decorator to get the title from the parent component
  @Input() data!: any[]; //input decorator to get the data from the parent component
  @Input() multiple = false; //input decorator to get the multiple from the parent component if the user wants to select multiple items
  @Input() itemTextField = 'name'; //input decorator to get the itemTextField from the parent component
  @Output() selectChanged: EventEmitter<any> = new EventEmitter(); //output decorator to emit the selected items to the parent component
  isOpen = false;
  selected: any = []; // create a variable to store the selected items
  filtered: any = []; // create a variable to store the filtered items
  constructor() {}

  //ngOnChanges allows us to detect the changes in the input properties and update the component accordingly
  //SimpleChanges is an interface that stores the previous and current values of the input properties
  ngOnChanges(changes: SimpleChanges): void {
    this.filtered = this.data; // set the filtered items to the data
    //we can't do this in ngOnInit because the data is not available at that time
    // also we can't do this in constructor because it is an async function and the data is not available at that time
  }

  openModal() {
    // create a function to open the modal
    this.isOpen = true;
  }

  cancelModal() {
    // create a function to select the items
    const selected = this.data.filter((item) => item.selected); // this function filters the data array and returns the items that are selected
    this.selectChanged.emit(selected); // emit the selected items to the parent component

    // create a function to cancel the modal
    this.isOpen = false;
  }
  select() {
    // create a function to select the items
    const selected = this.data.filter((item) => item.selected); // this function filters the data array and returns the items that are selected
    this.selectChanged.emit(selected); // emit the selected items to the parent component

    this.isOpen = false;
  }

  // create a leaf function to get the nested object value from the parent component
  leaf = (obj: any) =>
    this.itemTextField.split('.').reduce((value, el) => value[el], obj); // this function takes the object as parameter and runs split function on the itemTextField and then runs reduce function on the split array and returns the value of the nested object
  // reference: <https://stackoverflow.com/questions/8750362/using-variables-with-nested-javascript-object>
  // we use this function to dynamically get the value of the nested object from the parent component

  // create a function to check if the item is selected or not
  itemSelected() {
    this.selected = this.data.filter((item) => item.selected); // this function filters the data array and returns the items that are selected

    if (!this.multiple) {
      this.selectChanged.emit(this.selected); // if the multiple is false then emit the selected items to the parent component
    }
  }

  //create a function to filter and search the items
  filter(event: SearchbarCustomEvent): void {
    const filter = event.detail.value?.toLowerCase(); // get the value from the searchbar and convert it to lowercase

    this.filtered = this.data.filter((item) => {
      const itemValue = this.leaf(item).toLowerCase(); // get the value of the nested object and convert it to lowercase
      // if nothing is typed in the searchbar then return all the items
      if (!filter) {
        return true;
      } else {
        // if the searchbar is not empty then return the items that match the search query
        return itemValue.indexOf(filter) >= 0;
      }
    });
  }
}

Here is a brief overview of the above code:

  • The class has four @Input() properties that receive data and configuration options from the parent component: title, data, multiple, and itemTextField. The title property is a string that specifies the label of the select input. The data property is an array of objects that represents the list of items to select from. The multiple property is a boolean that determines whether the user can select multiple items. The itemTextField property is a string that specifies the name of the field to use for displaying the item text in the select input.
  • The class has one @Output() property called selectChanged that is an EventEmitter that emits an array of selected items to the parent component.
  • The class has three variables: isOpen, selected, and filtered. The isOpen variable is a boolean that keeps track of whether the select input is open or closed. The selected variable is an array that stores the selected items. The filtered variable is an array that stores the filtered items based on the user's search query.
  • The ngOnChanges() function is a lifecycle hook that detects changes in the input properties and updates the component accordingly. In this case, when the data property changes, the filtered variable is set to the data array.
  • The openModal() function sets the isOpen variable to true, which opens the select input modal.
  • The cancelModal() function filters the data array to get the selected items and emits them to the parent component using the selectChanged EventEmitter. It also sets the isOpen variable to false, which closes the select input modal.
  • The select() function is similar to the cancelModal() function, except that it does not cancel the modal. Instead, it just emits the selected items to the parent component and sets the isOpen variable to false.
  • The leaf() function is a helper function that is used to dynamically get the value of a nested object field from the parent component using the itemTextField property. It takes an object as a parameter and uses the split() and reduce() functions to access the nested value.
  • The itemSelected() function filters the data array to get the selected items and emits them to the parent component using the selectChanged EventEmitter. If the multiple property is false, it only emits the selected items once.
  • The filter() function is called when the user types in the searchbar input. It filters the data array based on the user's search query and stores the filtered items in the filtered variable. It uses the leaf() function to access the nested value of the itemTextField property. If the searchbar input is empty, it returns all the items. Otherwise, it returns the items that match the search query.

And finally, I will use the above logic to display the search bar and the list in searchable-select.component.html template in the following way:

<ion-list lines="full" *ngIf="selected.length; else placeholder">
  <ion-item lines="none">Selected Students: </ion-item>

  <ion-chip
    class="ion-margin-start"
    *ngFor="let item of selected"
    outline="false"
    color="tertiary"
  >
    <!-- <ion-icon name="person"></ion-icon> -->
    <ion-avatar>
      <img title="avatar" [src]="item.picture.thumbnail" />
    </ion-avatar>
    <ion-label>{{ leaf(item) }}</ion-label>
  </ion-chip>
</ion-list>

<ng-template #placeholder>
  <ion-item lines="none">
    <ion-label>No Student Selected</ion-label>
  </ion-item>
</ng-template>

<ion-modal [isOpen]="isOpen">
  <ng-template>
    <ion-header>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-button (click)="cancelModal()"> Cancel </ion-button>
        </ion-buttons>
        <ion-title>{{ title }}</ion-title>
        <ion-buttons slot="end">
          <ion-button (click)="select()"> Select </ion-button>
        </ion-buttons>
      </ion-toolbar>

      <ion-toolbar>
        <ion-searchbar
          aria-placeholder="Search"
          (ionInput)="filter($any($event))"
        ></ion-searchbar>
        <!-- Use ionInput instead of ionChange if you want the list to filter as you type otherwise you'll have to hit enter after typing -->
      </ion-toolbar>
    </ion-header>

    <ion-content>
      <ion-item
        lines="full"
        *ngFor="let item of filtered"
        (ionChange)="itemSelected()"
      >
        <ion-checkbox
          aria-label="selected"
          slot="start"
          [(ngModel)]="item.selected"
        ></ion-checkbox>
        <ion-label>{{ leaf(item) }}</ion-label>
      </ion-item>
    </ion-content>
  </ng-template>
</ion-modal>

The above template consists of two main parts.

The first part is a list of selected students displayed using the ion-list and ion-chip components. The *ngIf directive is used to conditionally display the list only if the selected array has at least one element. Otherwise, a placeholder is shown using an ng-template directive. The ngFor directive is used to iterate over the selected array and display a chip for each student.

The second part is an ion-modal component that displays a list of students. The modal is displayed based on the value of the isOpen property. The modal has a header containing a title and two buttons - cancel and select. It also has a search bar that can be used to filter the list of students displayed using an ngFor directive. Each student is displayed as an ion-item with a checkbox and a label. The ion-checkbox is used to allow the user to select or deselect a student, and the label displays the student's name. The (ionChange) event is used to detect when a student is selected, and the itemSelected() method is called to update the selected array.

Finally the feature is demonstrated in Figures 84, 85, and 86 below:

Figure 84: No student selected


Figure 85: Search & Select


Figure 86: Student Details


Conclusion

In this twenty-first installment of our series on building a multiplatform application with Angular-15 and Ionic-7, we explored the creation of a selectable and searchable data list using the Random User API and Angular's Input and Output decorators. By leveraging these powerful tools, we enhanced the interactivity and functionality of our Angular-15 Ionic-7 app, providing users with a dynamic and user-friendly data browsing experience.

Data lists are an integral part of many applications, allowing users to browse and interact with data efficiently. By integrating the Random User API, we generated a dataset of random user information to populate our data list, offering realistic and diverse data for users to explore.

Furthermore, by utilizing Angular's Input and Output decorators, we enabled data filtering and selection functionality within our app. Users can now search for specific data items and select them for further actions or display them in other parts of our application. The integration of these decorators enhanced the usability and interactivity of our app, providing a seamless user experience.

Throughout this tutorial, we provided guidance on component design, data handling, and user interaction, following best practices to ensure a well-structured and efficient implementation. By following along, you gained practical knowledge on how to create a selectable and searchable data list using the Random User API and Angular's Input and Output decorators.

We hope this tutorial has provided you with valuable insights and practical techniques for building a dynamic and user-friendly data browsing experience within your Angular-15 Ionic-7 app. By leveraging the power of the Random User API and Angular's decorators, you can enhance the interactivity and functionality of your application, ensuring a seamless and engaging user experience.

Thank you for joining us on this journey as we explored the fascinating world of app development with Angular-15, Ionic-7, the Random User API, and Angular's Input and Output decorators. Stay tuned for future installments where we will continue to explore new features and functionalities to make our app even more powerful, dynamic, and user-friendly.

Happy coding!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript