Data Table Column (or Entity Prop) Extensions for Angular UI

Introduction

Entity prop extension system allows you to add a new column to the data table for an entity or change/remove an already existing one. A "Name" column was added to the user management page below:

Entity Prop Extension Example: "Name" Column

You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells.

How to Set Up

In this example, we will add a "Name" column and display the value of the name field in the user management page of the Identity Module.

Step 1. Create Entity Prop Contributors

The following code prepares a constant named identityEntityPropContributors, ready to be imported and used in your root module:

// entity-prop-contributors.ts

import { EntityProp, EntityPropList, ePropType } from '@volo/abp.commercial.ng.ui';
import { Identity } from '@volo/abp.ng.identity';
import { IdentityEntityPropContributors } from '@volo/abp.ng.identity.config';

const nameProp = new EntityProp<Identity.UserItem>({
  type: ePropType.String,
  name: 'name',
  displayName: 'AbpIdentity::Name',
  sortable: true,
  columnWidth: 250,
});

export function namePropContributor(propList: EntityPropList<Identity.UserItem>) {
  propList.addAfter(
    nameProp,
    'userName',
    (value, name) => value.name === name,
  );
}

export const identityEntityPropContributors: IdentityEntityPropContributors = {
  'Identity.UsersComponent': [namePropContributor],
};

The list of props, conveniently named as propList, is a doubly linked list. That is why we have used the addAfter method, which adds a node with given value after the first node that has the previous value. You may find all available methods here.

Important Note 1: AoT compilation does not support function calls in decorator metadata. This is why we have defined namePropContributor as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to AoT metadata errors for details.

Important Note 2: Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.

export const identityEntityPropContributors: IdentityEntityPropContributors = {
  'Identity.UsersComponent': [ namePropContributor ],
};

/* OR */

const identityContributors: IdentityEntityPropContributors = {};
identityContributors[eIdentityComponents.Users] = [ namePropContributor ];
export const identityEntityPropContributors = identityContributors;

Step 2. Import and Use Entity Prop Contributors

Import identityEntityPropContributors in your root module and pass it to the static forRoot method of IdentityConfigModule as seen below:

import { IdentityConfigModule } from '@volo/abp.ng.identity.config';
import { identityEntityPropContributors } from './entity-prop-contributors';

@NgModule({
  imports: [
    // Other imports

    IdentityConfigModule.forRoot({
      entityPropContributors: identityEntityPropContributors,
    }),

    // Other imports
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}

That is it, nameProp entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (UsersComponent) of the IdentityModule.

How to Render Custom HTML in Cells

You can use the valueResolver to render an HTML string in the table. Imagine we want to show a red times icon (❌) next to unconfirmed emails and phones, instead of showing a green check icon next to confirmed emails and phones. The contributors below would do that for you.

// entity-prop-contributors.ts

import { EntityProp, EntityPropList, ePropType } from '@volo/abp.commercial.ng.ui';
import { Identity } from '@volo/abp.ng.identity';
import { IdentityEntityPropContributors } from '@volo/abp.ng.identity.config';

export function emailPropContributor(propList: EntityPropList<Identity.UserItem>) {
  const index = propList.indexOf('email', (value, name) => value.name === name);
  const droppedNode = propList.dropByIndex(index);
  const emailProp = new EntityProp<Identity.UserItem>({
    ...droppedNode.value,
    valueResolver: data => {
      const { email, emailConfirmed } = data.record;
      const icon = email && !emailConfirmed ? `<i class="fa fa-times text-danger ml-1"></i>` : '';

      return of((email || '') + icon); // should return an observable
    },
  });

  propList.addByIndex(emailProp, index);
}

export function phonePropContributor(propList: EntityPropList<Identity.UserItem>) {
  const index = propList.indexOf('phoneNumber', (value, name) => value.name === name);
  const droppedNode = propList.dropByIndex(index);
  const phoneProp = new EntityProp<Identity.UserItem>({
    ...droppedNode.value,
    valueResolver: data => {
      const { phoneNumber, phoneNumberConfirmed } = data.record;
      const icon =
        phoneNumber && !phoneNumberConfirmed ? `<i class="fa fa-times text-danger ml-1"></i>` : '';

      return of((phoneNumber || '') + icon); // should return an observable
    },
  });

  propList.addByIndex(phoneProp, index);
}

export const identityEntityPropContributors: IdentityEntityPropContributors = {
  'Identity.UsersComponent': [emailPropContributor, phonePropContributor],
};

The valueResolver method should return an observable. You can wrap your return values with of from RxJS for that.

API

PropData<R = any>

PropData is the shape of the parameter passed to all callbacks or predicates in an EntityProp.

It has the following properties:

  • record is the row data, i.e. current value rendered in the table.

    {
      type: ePropType.String,
      name: 'name',
      valueResolver: data => {
        const name = data.record.name || '';
        return of(name.toUpperCase());
      },
    }
    
  • index is the table index where the record is at.

  • getInjected is the equivalent of Injector.get. You can use it to reach injected dependencies of ExtensibleTableComponent, including, but not limited to, its parent component.

    {
      type: ePropType.String,
      name: 'name',
      valueResolver: data => {
        const restService = data.getInjected(RestService);
        const usersComponent = data.getInjected(UsersComponent);
    
        // Use restService and usersComponent public props and methods here
      },
    }
    

PropCallback<T, R = any>

PropCallback is the type of the callback function that can be passed to an EntityProp as prop parameter. A prop callback gets a single parameter, the PropData. The return type may be anything, including void. Here is a simplified representation:

type PropCallback<T, R = any> = (data?: PropData<T>) => R;

PropPredicate<T>

PropPredicate is the type of the predicate function that can be passed to an EntityProp as visible parameter. A prop predicate gets a single parameter, the PropData. The return type must be boolean. Here is a simplified representation:

type PropPredicate<T> = (data?: PropData<T>) => boolean;

EntityPropOptions<R = any>

EntityPropOptions is the type that defines required and optional properties you have to pass in order to create an entity prop.

Its type definition is as follows:

type EntityPropOptions<R = any> = {
  type: ePropType;
  name: string;
  displayName?: string;
  valueResolver?: PropCallback<R, Observable<any>>;
  sortable?: boolean;
  columnWidth?: number;
  permission?: string;
  visible?: PropPredicate<R>;
};

As you see, passing type and name is enough to create an entity prop. Here is what each property is good for:

  • type is the type of the prop value. It is used for custom rendering in the table. (required)
  • name is the property name (or key) which will be used to read the value of the prop. (required)
  • displayName is the name of the property which will be localized and shown as column header. (default: options.name)
  • valueResolver is a callback that is called when the cell is rendered. It must return an observable. (default: data => of(data.record[options.name]))
  • sortable defines if the table is sortable based on this entity prop. Sort icons are shown based on it. (default: false)
  • columnWidth defines a minimum width for the column. Good for horizontal scroll. (default: undefined)
  • permission is the permission context which will be used to decide if a column for this entity prop should be displayed to the user or not. (default: undefined)
  • visible is a predicate that will be used to decide if this entity prop should be displayed on the table or not. (default: () => true)

Important Note: Do not use record in visibility predicates. First of all, the table header checks it too and the record will be undefined. Second, if some cells are displayed and others are not, the table will be broken. Use the valueResolver and render an empty cell when you need to hide a specific cell.

You may find a full example below.

EntityProp<R = any>

EntityProp is the class that defines your entity props. It takes an EntityPropOptions and sets the default values to the properties, creating an entity prop that can be passed to an entity contributor.

const options: EntityPropOptions<Identity.UserItem> = {
  type: ePropType.String,
  name: 'email',
  displayName: 'AbpIdentity::EmailAddress',
  valueResolver: data => {
    const { email, emailConfirmed } = data.record;

    return of(
      (email || '') + (emailConfirmed ? `<i class="fa fa-check text-success ml-1"></i>` : ''),
    );
  },
  sortable: true,
  columnWidth: 250,
  permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical
  visible: data => {
    const store = data.getInjected(Store);
    const selectSensitiveDataVisibility = ConfigState.getSetting(
      'Abp.Identity.IsSensitiveDataVisible'  // hypothetical
    );
    return store.selectSnapshot(selectSensitiveDataVisibility);
  }
};

const prop = new EntityProp(options);

It also has two static methods to create its instances:

  • EntityProp.create<R = any>(options: EntityPropOptions<R>) is used to create an instance of EntityProp.
    const prop = EntityProp.create(options);
    
  • EntityProp.createMany<R = any>(options: EntityPropOptions<R>[]) is used to create multiple instances of EntityProp with given array of EntityPropOptions.
    const props = EntityProp.createMany(optionsArray);
    

EntityPropList<R = any>

EntityPropList is the list of props passed to every prop contributor callback as the first parameter named propList. It is a doubly linked list. You may find all available methods here.

The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:

export function reorderUserContributors(
  propList: EntityPropList<Identity.UserItem>,
) {
  // drop email node
  const emailPropNode = propList.dropByValue(
    'AbpIdentity::EmailAddress',
    (prop, text) => prop.text === text,
  );

  // add it back after phoneNumber
  propList.addAfter(
    emailPropNode.value,
    'phoneNumber',
    (value, name) => value.name === name,
  );
}

EntityPropContributorCallback<R = any>

EntityPropContributorCallback is the type that you can pass as entity prop contributor callbacks to static forRoot methods of the modules.

export function isLockedOutPropContributor(
  propList: EntityPropList<Identity.UserItem>,
) {
  // add isLockedOutProp as 2nd column
  propList.add(isLockedOutProp).byIndex(1);
}

export const identityEntityPropContributors = {
  [eIdentityComponents.Users]: [isLockedOutPropContributor],
};

See Also

In this document