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
, anditemTextField
. 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 aboolean
that determines whether the user can select multiple items. TheitemTextField
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 calledselectChanged
that is anEventEmitter
that emits an array of selected items to the parent component. - The class has three variables:
isOpen
,selected
, andfiltered
. TheisOpen
variable is aboolean
that keeps track of whether the select input is open or closed. Theselected
variable is an array that stores the selected items. Thefiltered
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 theisOpen
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 theselectChanged
EventEmitter
. It also sets theisOpen
variable to false, which closes the select input modal. - The
select()
function is similar to thecancelModal()
function, except that it does not cancel the modal. Instead, it just emits the selected items to the parent component and sets theisOpen
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 theitemTextField
property. It takes an object as a parameter and uses thesplit()
andreduce()
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 theselectChanged
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 theleaf()
function to access the nested value of theitemTextField
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!