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
ListComponentclass, and passed a type parameterCustomerto it, meaning that we are creating a class that represents a Skyframe list component for theCustomerentity. - 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'sActivatedRouteprovider. Used for navigation.
- We added a
super()call to the constructor and passed theCustomerclass as the first parameter. With this, we are making theshellknow 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 theViewbutton that navigates to the detail page of an entity. It invokes thetriggerUseCase()method of theShell. You can go to the Shell guide to learn how it works: Shell.addNewEntity(): void: We use this method in the template to create theAddbutton of a cart mode list. It instantiates a new entity (Customer) through theShell, then it utilizes theaddToCart()method inherited byListComponentto interact with the cart items. We will explain all the available methods fromListComponentin 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
formControlNameinput provided by theFormControlNamedirective binds each individual input to the form control defined inFormGroup. 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 theentitiesinput is not provided): Fetches the data from the backend.FrontendListDataSource(if theentitiesinput 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... */ }}