Skyframe Documentation

List

List

This guide explains how to create a standard Skyframe list component.

What is a list component?

A list component is a container component that holds a series of entity instances. It can be used to display the values of a collection of entities, create a component that lets you select an item (or multiple items) from a set of options, or build a shopping-cart-like component that gives you the ability to add/remove items from a list.

What are the benefits?

  • Entity page retrieval from a Skyframe backend API
  • Out-of-the-box support for pagination
  • Convenience methods for filtering or searching items
  • Switching modes (display, single/multiple select or cart) easily

List modes

There are four modes of a Skyframe list component:

  • Display: A simple view for a collection of entities, no editable form fields, no add/remove options, just display.
  • Single-select: Besides showing a collection of entities, it adds a radio button to each row to let a user to choose an entity.
  • Multiple-select: Like single select, but instead of adding a radio button, it adds a checkbox to each row, so the user is able to choose more than one entities.
  • Cart: With buttons for add and remove actions

Creating a Skyframe list component

Entity definition

Say we have a Skyframe entity Customer with this definition below:

In our shared domain

@demo-project/shared/src/model/Customer.ts:

@Entity()
export class Customer {
@PrimaryKey()
id: number;
@Field()
email: string;
@Field()
firstName: string;
@Field()
lastName: string;
}

The angular entity

@demo-project/shared/src/model/Customer.ts:

export class Customer extends AngularEntity(SharedModels.Customer) {}

You may want to learn how to create a Skyframe entity by reading the Domain Definition guide.

Create the Angular component

Just like any other Angular component, a standard Skyframe list component should have at least a component class (a TypeScript class) and a template (Angular HTML Template):

Template

@demo-project/frontend/src/app/modules/customer/components/list/list.html:

<!-- If this is a cart mode list, create an add button. -->
<button
*ngIf="mode?.type === 'cart'"
class="btn btn-sm m-0 btn-outline-secondary"
(click)="addNewEntity()"
>
Add
</button>
<table class="skf-table">
<thead>
<tr>
<ng-container
*ngIf="mode?.type === 'single-select' || mode?.type === 'multi-select'"
>
<!--
If this is a single/multiple-select list, create an empty
column header for the checkbox/radio button.
-->
<th></th>
</ng-container>
<ng-container *ngIf="mode?.type != 'cart'">
<!-- Add column headers (Non-cart mode) -->
<th>Id</th>
<th>Email</th>
<th>First Name</th>
<th>Last Name</th>
</ng-container>
<ng-container *ngIf="mode?.type === 'cart'">
<!-- Add column headers (Cart mode) -->
<!-- No `id` field because it is not an editable field -->
<th>Email</th>
<th>First Name</th>
<th>Last Name</th>
</ng-container>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!--
Show a loading state indicator if the page data is not ready yet.
When the page data is loaded, the list view would be shown.
-->
<ng-template
[ngIf]="loading | async"
[ngIfElse]="listView">
<tr>
<td colspan="100">
<app-data-loading></app-data-loading>
</td>
</tr>
</ng-template>
</tbody>
</table>
<ng-template listView>
<!--
`rows` is an [Observable](https://angular.io/guide/observables-in-angular) of an array of entities (Customers in this example).
We are applying the [async pipe](https://angular.io/guide/observables-in-angular#async-pipe) (`| async`) to `rows` to unwrap the result
of the rows of entities we fetched from the data source.
-->
<ng-container *ngIf="rows | async as entities">
<tr *ngFor="let entity of entities; index as i">
<ng-container [ngSwitch]="mode?.type">
<ng-container *ngSwitchCase="'single-select'">
<td>
<!-- If this is single-select list, create a radio button. -->
<input
type="radio"
[name]="formList.name"
[checked]="mode.isSelected(entity)"
(change)="mode.select(entity)"
class="form-checkbox"
/>
</td>
</ng-container>
<ng-container *ngSwitchCase="'multi-select'">
<td>
<!-- If this is a multiple-select list, create a checkbox. -->
<input
type="checkbox"
[name]="formList.name"
[checked]="mode.isSelected(entity)"
(change)="
mode.isSelected(entity)
? mode.unselect(entity)
: mode.select(entity)
"
/>
</td>
</ng-container>
</ng-container>
<ng-container [ngSwitch]="mode?.type">
<ng-container *ngSwitchCase="'cart'" [formGroup]="entity">
<!--
List row content template (Cart mode)
We want to editable form fields here.
We make use of Angular's `formControlName` directive to
bind form controls to the native input element
Note that `entity` is a FormGroup instance here.
-->
<td>
<input id="email" formControlName="email" type="text" placeholder="Email" class="form-control" />
</td>
<td>
<input id="firstName" formControlName="firstName" type="text" placeholder="First Name" class="form-control" />
</td>
<td>
<input id="lastName" formControlName="lastName" type="text" placeholder="Last Name" class="form-control" />
</td>
</ng-container>
<ng-container *ngSwitchDefault>
<!--
List row content template (Non-cart mode)
We only want to display field values here.
Note that `entity` is an instance of `Customer` here.
-->
<td> {{ entity.id }} </td>
<td> {{ entity.email }} </td>
<td> {{ entity.firstName }} </td>
<td> {{ entity.lastName }} </td>
</ng-container>
<td>
<ng-container [ngSwitch]="mode?.type">
<ng-container *ngSwitchCase="'cart'">
<!-- If this is a cart mode list, create an remove button. -->
<button
class="btn btn-sm m-0 btn-outline-secondary"
(click)="removeFromCartAt(i)"
>
Remove
</button>
</ng-container>
<ng-container *ngSwitchDefault>
<!-- Create a button for navigation to entity's detail page. -->
<button
class="btn btn-sm m-0 btn-outline-secondary"
(click)="viewDetails(entity.id)"
>
View
</button>
</ng-container>
</ng-container>
</td>
</tr>
<tr *ngIf="entities?.length === 0">
<td colspan="100">
<i>No items</i>
</td>
</tr>
</ng-container>
</ng-template>
<!-- Add pagination to our list component. -->
<app-pagination *ngIf="mode?.type !== 'cart'"></app-pagination>

Pay attention to the highlighted parts. You surely want to customize these parts to rename the column headers, include or exclude some fields, create custom columns, use some fancy form input components, etc.

Component class

Let's create a new Angular component class:

@demo-project/frontend/src/app/modules/customer/components/list/list.ts:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'demo-project-customer-list',
templateUrl: './list.html'
})
export class CustomerListComponent {}

Now we want to add all the functionalities of Skyframe list component by subclassing our class to ListComponent:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ListComponent, Shell } from '@skyframe/angular';
import { Customer } from '../../customer';
@Component({
selector: 'demo-project-customer-list',
templateUrl: './list.html'
})
export class CustomerListComponent extends ListComponent<Customer> {
constructor(shell: Shell, activatedRoute: ActivatedRoute) {
super(Customer, shell, activatedRoute);
}
}

What we did here?

  • We extended the ListComponent class, and passed a type parameter Customer to it, meaning that we are creating a class that represents a Skyframe list component for the Customer entity.
  • We added a constructor, which receives a two parameters:
    • shell: Shell: It is the orchestrator object of a Skyframe web app. It holds the context of the whole application, and it allows the list component to fetch data from the backend, know everything about a Skyframe entity, navigate to other pages, etc.
    • activatedRoute: ActivatedRoute: Angular's ActivatedRoute provider. Used for navigation.
  • We added a super() call to the constructor and passed the Customer class as the first parameter. With this, we are making the shell know what entity we are working with.

We are done creating our first Skyframe list component! Now we have to add some methods we are using inside the HTML template:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ListComponent, Shell } from '@skyframe/angular';
import { Customer } from '../../customer';
@Component({
selector: 'demo-project-customer-list',
templateUrl: './list.html'
})
export class CustomerListComponent extends ListComponent<Customer> {
constructor(shell: Shell, activatedRoute: ActivatedRoute) {
super(Customer, shell, activatedRoute);
}
public viewDetails(entityId: number): void {
this.shell.triggerUseCase(Customer, 'detail', [entityId]);
}
public addNewEntity(): void {
const newEntity = this.shell.createEntity(Customer, {});
this.addToCart(newEntity);
}
}

We created two methods:

  • viewDetails(entityId: number): void: We use this method in the template to create the View button that navigates to the detail page of an entity. It invokes the triggerUseCase() method of the Shell. You can go to the Shell guide to learn how it works: Shell.
  • addNewEntity(): void: We use this method in the template to create the Add button of a cart mode list. It instantiates a new entity (Customer) through the Shell, then it utilizes the addToCart() method inherited by ListComponent to interact with the cart items. We will explain all the available methods from ListComponent in the next section.

Importing our component into the entity module

Let's import our list component into the CustomerModule. To do so, we should add the ListModule from @skyframe/angular package to the NgModule's imports. It adds all the providers required by the Skyframe list component. Then we have to include the CustomerListComponent class we created in the previous step to declarations and exports to tell Angular that this component is part of the module.

@demo-project/frontend/src/app/modules/customer/customer-routing.module.ts:

import { NgModule } from '@angular/core';
import { ListModule, Shell, SkfModule } from '@skyframe/angular';
import { CustomerListComponent } from './components/list/list';
import { Customer } from './customer';
@NgModule({
imports: [
...
ListModule
],
declarations: [
...
CustomerListComponent
],
entryComponents: [
...
],
exports: [
...
CustomerListComponent
]
})
@SkfModule({
entities: [Customer]
})
export class CustomerModule {
constructor(private shell: Shell) { ... }
}

Using the component in templates

Display list

Fetch page from backend

If you are not passing any options to the list, then it would behave as a display list that fetches the data from Skyframe backend API.

<demo-project-customer-list></demo-project-customer-list>

Pass in an array of customers

You can pass in an array of customers through the entities input.

<demo-project-customer-list
[entities]="arrayOfCustomers"
></demo-project-customer-list>

Single-select list

Fetch options from backend

Now we are passing a mode to the list component with "single-select" value, indicating that it should be a single-select list. We are also passing a skfFormControlName directive to it, don't worry about that, we will explain that later.

<demo-project-customer-list
mode="single-select"
skfFormControlName="customer"
></demo-project-customer-list>

Pass in an array of customers as options

Like display list, you can pass in an array through the entities input.

<demo-project-customer-list
mode="single-select"
skfFormControlName="customer"
[entities]="arrayOfCustomers"
></demo-project-customer-list>

Multiple-select list

Fetch options from backend

It's almost the same as the single-select list, except that, instead of passing a "single-select" mode, we set a "multi-select" to it.

<demo-project-customer-list
mode="multi-select"
skfFormControlName="customers"
></demo-project-customer-list>

Pass in an array of customers as options

Yes, the entities input also applies to a multiple-select list.

<demo-project-customer-list
mode="multi-select"
skfFormControlName="customers"
[entities]="arrayOfCustomers"
></demo-project-customer-list>

Cart list

It's as simple as passing it a "cart" value to mode input.

<demo-project-customer-list
mode="cart"
skfFormControlName="customers"
></demo-project-customer-list>

What is the skfFormControlName directive?

It is an Angular directive with similar usage to formControlName directive for Angular Reactive Forms.

What does formControlName do?

According to the Angular documentation:

The formControlName input provided by the FormControlName directive binds each individual input to the form control defined in FormGroup. The form controls communicate with their respective elements. They also communicate changes to the form group instance, which provides the source of truth for the model value.

In brief, the formControlName lets you bind a form control inside the form group to an input element, so when the user interacts with the input element in the page and modifies its value, these changes would apply to the form group:

<!-- The `formGroup` directive provides a form group to the inner elements. -->
<form [formGroup]="customerFormGroup">
<!--
The `formControlName` references each form control inside
the `customerFormGroup` by their names.
-->
<input id="email" type="text" formControlName="email" />
<input id="firstName" type="text" formControlName="firstName" />
<input id="lastName" type="text" formControlName="lastName" />
</form>

What does skfFormControlName do?

While Angular's formControlName helps us to bind FormControl instances to an input element, the skfFormControlName is useful for binding FormGroup or FormArray instances inside a parent FormGroup:

<!-- The `formGroup` directive provides a form group to the inner elements. -->
<form [formGroup]="formGroup">
<!--
Whenever the user selects an customer, `formGroup.customer` would
result in a form group holding the customer's values.
-->
<demo-project-customer-list
mode="single-select"
skfFormControlName="customer"
></demo-project-customer-list>
<!--
Whenever the user selects new customer items, `formGroup.customers` would
result in a form array instance holding all the selected customers'
form groups.
-->
<demo-project-customer-list
mode="multi-select"
skfFormControlName="customers"
></demo-project-customer-list>
<!--
Whenever the user edits the cart items, `formGroup.customers` would
result in a form array instance holding all the created customers'
form groups.
-->
<demo-project-customer-list
mode="cart"
skfFormControlName="customers"
></demo-project-customer-list>
</form>

The list data source

Internally, the ListComponent delegates the responsability to fetch the list page items to the ListDataSource. There are two types of ListDataSource:

  • ServerSideListDataSource (if the entities input is not provided): Fetches the data from the backend.
  • FrontendListDataSource (if the entities input is provided): Uses the given array of entities as the source of data.

Public API

Inputs

@Input('mode'): One of 'cart', 'multi-select' and 'single-select'. Used to specify a non-display mode of the list. If no mode is given, it would be a display list.

@Input('entities'): Accepts an array of entities (T[]). If provided, the list would use the given array of entities as its data source instead of fetching them from the backend.

TypeScript methods

Cart list

The following methods are useful if you are working with a cart list:

// Adds an entity instance to the cart.
public addToCart(entity: T): void;
// Inserts an entity instance to the cart at the given index.
public insertToCart(index: number, entity: T): void;
// Removes the entity instance in the given index.
public removeFromCartAt(index: number): void;
// Clears all the cart items.
public clearCart(): void;

Filtering

You can apply filters to the list by calling the setFilters() method:

public setFilters(filters: EntityFormGroup<T>): void;

Once you call the setFilters() method, it will iterate through the filters, the form group instance you passed in, then filter the list elements by the values given by the form group instance.

Also, there is a method that allows you to filter the list elements by a search query string:

public setSearchQuery(searchQuery: string | null): void;

The setSearchQuery() iterates through all the queryable fields of each element (all the string and number fields, for example, id, email, firstName and lastName fields of Customer), and if at least one of these fields' value partially matches the searchQuery (for example, searchQuery is 'ell' and the element has a field with value 'HELLO'), it would be included in the search result.

State management

The following methods are used for managing the component state:

// Indicates the loading state of the list data.
protected setLoading(loading: boolean): void;
// Sets the rows that the list would show.
// This method should be called by the list's data source to update the list content.
// We don't recommend you to call this method explicitly.
protected setRows(rows: Array<T | EntityFormGroup<T>>): void;

Angular lifecycle hooks

These are the Angular lifecycle hooks defined in ListComponent:

  • ngOnInit: For component state initialization.
  • ngAfterViewInit: For initializing list mode and data source related subscriptions.
  • ngOnDestroy: For disposing subscriptions and destroying component state.

If you are overriding one of these methods, please make sure that you are calling the parent method, otherwise, the ListComponent would not work as expected.

import { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
export class MyListComponent
extends ListComponent<MyEntity>
implements OnInit, AfterViewInit, OnDestroy {
public ngOnInit(): void {
super.ngOnInit();
/* Do something else... */
}
public ngAfterViewInit(): void {
super.ngAfterViewInit();
/* Do something else... */
}
public ngOnDestroy(): void {
super.ngOnDestroy();
/* Do something else... */
}
}